Skip to content

Commit ebdf3a8

Browse files
authored
Support disabling tool suggest for specific tools. (#20072)
## Summary - Add `disable_tool_suggest` to app and plugin config, schema, and TypeScript output - Exclude disabled connectors and plugins from tool suggestion discovery - Persist "never show again" tool-suggestion choices back into `config.toml` - Update config docs and add coverage for connector and plugin suppression ## Testing - Added and updated unit tests for config persistence and tool-suggest filtering - Not run (not requested)
1 parent 1211a90 commit ebdf3a8

17 files changed

Lines changed: 681 additions & 6 deletions

File tree

codex-rs/config/src/types.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,45 @@ pub struct ToolSuggestDiscoverable {
186186
pub id: String,
187187
}
188188

189+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
190+
#[schemars(deny_unknown_fields)]
191+
pub struct ToolSuggestDisabledTool {
192+
#[serde(rename = "type")]
193+
pub kind: ToolSuggestDiscoverableType,
194+
pub id: String,
195+
}
196+
197+
impl ToolSuggestDisabledTool {
198+
pub fn plugin(id: impl Into<String>) -> Self {
199+
Self {
200+
kind: ToolSuggestDiscoverableType::Plugin,
201+
id: id.into(),
202+
}
203+
}
204+
205+
pub fn connector(id: impl Into<String>) -> Self {
206+
Self {
207+
kind: ToolSuggestDiscoverableType::Connector,
208+
id: id.into(),
209+
}
210+
}
211+
212+
pub fn normalized(&self) -> Option<Self> {
213+
let id = self.id.trim();
214+
(!id.is_empty()).then(|| Self {
215+
kind: self.kind,
216+
id: id.to_string(),
217+
})
218+
}
219+
}
220+
189221
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
190222
#[schemars(deny_unknown_fields)]
191223
pub struct ToolSuggestConfig {
192224
#[serde(default)]
193225
pub discoverables: Vec<ToolSuggestDiscoverable>,
226+
#[serde(default)]
227+
pub disabled_tools: Vec<ToolSuggestDisabledTool>,
194228
}
195229

196230
/// Memories settings loaded from config.toml.

codex-rs/core/config.schema.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2169,6 +2169,13 @@
21692169
"ToolSuggestConfig": {
21702170
"additionalProperties": false,
21712171
"properties": {
2172+
"disabled_tools": {
2173+
"default": [],
2174+
"items": {
2175+
"$ref": "#/definitions/ToolSuggestDisabledTool"
2176+
},
2177+
"type": "array"
2178+
},
21722179
"discoverables": {
21732180
"default": [],
21742181
"items": {
@@ -2179,6 +2186,22 @@
21792186
},
21802187
"type": "object"
21812188
},
2189+
"ToolSuggestDisabledTool": {
2190+
"additionalProperties": false,
2191+
"properties": {
2192+
"id": {
2193+
"type": "string"
2194+
},
2195+
"type": {
2196+
"$ref": "#/definitions/ToolSuggestDiscoverableType"
2197+
}
2198+
},
2199+
"required": [
2200+
"id",
2201+
"type"
2202+
],
2203+
"type": "object"
2204+
},
21822205
"ToolSuggestDiscoverable": {
21832206
"additionalProperties": false,
21842207
"properties": {

codex-rs/core/src/config/config_tests.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ use codex_config::types::NotificationMethod;
4646
use codex_config::types::Notifications;
4747
use codex_config::types::SandboxWorkspaceWrite;
4848
use codex_config::types::SkillsConfig;
49+
use codex_config::types::ToolSuggestDisabledTool;
4950
use codex_config::types::ToolSuggestDiscoverableType;
5051
use codex_config::types::Tui;
5152
use codex_config::types::TuiKeymap;
@@ -8144,6 +8145,7 @@ discoverables = [
81448145
id: " ".to_string(),
81458146
},
81468147
],
8148+
disabled_tools: Vec::new(),
81478149
})
81488150
);
81498151

@@ -8168,11 +8170,118 @@ discoverables = [
81688170
id: "plugin_alpha@openai-curated".to_string(),
81698171
},
81708172
],
8173+
disabled_tools: Vec::new(),
81718174
}
81728175
);
81738176
Ok(())
81748177
}
81758178

8179+
#[tokio::test]
8180+
async fn tool_suggest_disabled_tools_load_from_config_toml() -> std::io::Result<()> {
8181+
let cfg: ConfigToml = toml::from_str(
8182+
r#"
8183+
[tool_suggest]
8184+
disabled_tools = [
8185+
{ type = "connector", id = " connector_calendar " },
8186+
{ type = "connector", id = "connector_calendar" },
8187+
{ type = "connector", id = " " },
8188+
{ type = "plugin", id = "slack@openai-curated" }
8189+
]
8190+
"#,
8191+
)
8192+
.expect("TOML deserialization should succeed");
8193+
8194+
assert_eq!(
8195+
cfg.tool_suggest,
8196+
Some(ToolSuggestConfig {
8197+
discoverables: Vec::new(),
8198+
disabled_tools: vec![
8199+
ToolSuggestDisabledTool::connector(" connector_calendar "),
8200+
ToolSuggestDisabledTool::connector("connector_calendar"),
8201+
ToolSuggestDisabledTool::connector(" "),
8202+
ToolSuggestDisabledTool::plugin("slack@openai-curated"),
8203+
],
8204+
})
8205+
);
8206+
8207+
let codex_home = TempDir::new()?;
8208+
let config = Config::load_from_base_config_with_overrides(
8209+
cfg,
8210+
ConfigOverrides::default(),
8211+
codex_home.abs(),
8212+
)
8213+
.await?;
8214+
8215+
assert_eq!(
8216+
config.tool_suggest,
8217+
ToolSuggestConfig {
8218+
discoverables: Vec::new(),
8219+
disabled_tools: vec![
8220+
ToolSuggestDisabledTool::connector("connector_calendar"),
8221+
ToolSuggestDisabledTool::plugin("slack@openai-curated"),
8222+
],
8223+
}
8224+
);
8225+
Ok(())
8226+
}
8227+
8228+
#[tokio::test]
8229+
async fn tool_suggest_disabled_tools_merge_across_config_layers() -> std::io::Result<()> {
8230+
let codex_home = TempDir::new()?;
8231+
let workspace = TempDir::new()?;
8232+
let workspace_key = workspace.path().to_string_lossy().replace('\\', "\\\\");
8233+
std::fs::write(
8234+
codex_home.path().join(CONFIG_TOML_FILE),
8235+
format!(
8236+
r#"
8237+
[projects."{workspace_key}"]
8238+
trust_level = "trusted"
8239+
8240+
[tool_suggest]
8241+
disabled_tools = [
8242+
{{ type = "connector", id = " user_connector " }},
8243+
{{ type = "plugin", id = "shared_plugin" }},
8244+
{{ type = "connector", id = "project_connector" }},
8245+
]
8246+
"#
8247+
),
8248+
)?;
8249+
8250+
let project_config_dir = workspace.path().join(".codex");
8251+
std::fs::create_dir_all(&project_config_dir)?;
8252+
std::fs::write(
8253+
project_config_dir.join(CONFIG_TOML_FILE),
8254+
r#"
8255+
[tool_suggest]
8256+
disabled_tools = [
8257+
{ type = "connector", id = "project_connector" },
8258+
{ type = "plugin", id = "project_plugin" },
8259+
{ type = "plugin", id = "shared_plugin" },
8260+
]
8261+
"#,
8262+
)?;
8263+
8264+
let config = ConfigBuilder::without_managed_config_for_tests()
8265+
.codex_home(codex_home.path().to_path_buf())
8266+
.harness_overrides(ConfigOverrides {
8267+
cwd: Some(workspace.path().to_path_buf()),
8268+
..Default::default()
8269+
})
8270+
.build()
8271+
.await?;
8272+
8273+
assert_eq!(
8274+
config.tool_suggest.disabled_tools,
8275+
vec![
8276+
ToolSuggestDisabledTool::connector("user_connector"),
8277+
ToolSuggestDisabledTool::plugin("shared_plugin"),
8278+
ToolSuggestDisabledTool::connector("project_connector"),
8279+
ToolSuggestDisabledTool::plugin("project_plugin"),
8280+
]
8281+
);
8282+
Ok(())
8283+
}
8284+
81768285
#[tokio::test]
81778286
async fn experimental_realtime_start_instructions_load_from_config_toml() -> std::io::Result<()> {
81788287
let cfg: ConfigToml = toml::from_str(

codex-rs/core/src/config/edit.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ use crate::path_utils::write_atomically;
33
use anyhow::Context;
44
use codex_config::CONFIG_TOML_FILE;
55
use codex_config::types::McpServerConfig;
6+
use codex_config::types::ToolSuggestDisabledTool;
67
use codex_features::FEATURES;
78
use codex_protocol::config_types::Personality;
89
use codex_protocol::config_types::ServiceTier;
910
use codex_protocol::config_types::TrustLevel;
1011
use codex_protocol::openai_models::ReasoningEffort;
1112
use std::collections::BTreeMap;
1213
use std::collections::HashMap;
14+
use std::collections::HashSet;
1315
use std::path::Path;
1416
use std::path::PathBuf;
1517
use tokio::task;
@@ -57,6 +59,8 @@ pub enum ConfigEdit {
5759
RecordModelMigrationSeen { from: String, to: String },
5860
/// Replace the entire `[mcp_servers]` table.
5961
ReplaceMcpServers(BTreeMap<String, McpServerConfig>),
62+
/// Add a disabled tool suggestion under `[tool_suggest].disabled_tools`.
63+
AddToolSuggestDisabledTool(ToolSuggestDisabledTool),
6064
/// Set or clear a skill config entry under `[[skills.config]]` by path.
6165
SetSkillConfig { path: PathBuf, enabled: bool },
6266
/// Set or clear a skill config entry under `[[skills.config]]` by name.
@@ -180,10 +184,13 @@ mod document_helpers {
180184
use codex_config::types::McpServerEnvVar;
181185
use codex_config::types::McpServerToolConfig;
182186
use codex_config::types::McpServerTransportConfig;
187+
use codex_config::types::ToolSuggestDisabledTool;
188+
use codex_config::types::ToolSuggestDiscoverableType;
183189
use toml_edit::Array as TomlArray;
184190
use toml_edit::InlineTable;
185191
use toml_edit::Item as TomlItem;
186192
use toml_edit::Table as TomlTable;
193+
use toml_edit::Value as TomlValue;
187194
use toml_edit::value;
188195

189196
pub(super) fn ensure_table_for_write(item: &mut TomlItem) -> Option<&mut TomlTable> {
@@ -379,6 +386,57 @@ mod document_helpers {
379386
table
380387
}
381388

389+
pub(super) fn parse_tool_suggest_disabled_tool(
390+
value: &TomlValue,
391+
) -> Option<ToolSuggestDisabledTool> {
392+
let table = value.as_inline_table()?;
393+
let kind = match table.get("type").and_then(TomlValue::as_str) {
394+
Some("connector") => ToolSuggestDiscoverableType::Connector,
395+
Some("plugin") => ToolSuggestDiscoverableType::Plugin,
396+
_ => return None,
397+
};
398+
let id = table.get("id").and_then(TomlValue::as_str)?;
399+
Some(ToolSuggestDisabledTool {
400+
kind,
401+
id: id.to_string(),
402+
})
403+
}
404+
405+
pub(super) fn parse_tool_suggest_disabled_tool_table(
406+
table: &TomlTable,
407+
) -> Option<ToolSuggestDisabledTool> {
408+
let kind = match table.get("type").and_then(TomlItem::as_str) {
409+
Some("connector") => ToolSuggestDiscoverableType::Connector,
410+
Some("plugin") => ToolSuggestDiscoverableType::Plugin,
411+
_ => return None,
412+
};
413+
let id = table.get("id").and_then(TomlItem::as_str)?;
414+
Some(ToolSuggestDisabledTool {
415+
kind,
416+
id: id.to_string(),
417+
})
418+
}
419+
420+
pub(super) fn tool_suggest_disabled_tools_value(
421+
disabled_tools: &[ToolSuggestDisabledTool],
422+
) -> TomlItem {
423+
let mut array = TomlArray::new();
424+
for disabled_tool in disabled_tools {
425+
let mut table = InlineTable::new();
426+
table.insert(
427+
"type",
428+
match disabled_tool.kind {
429+
ToolSuggestDiscoverableType::Connector => "connector",
430+
ToolSuggestDiscoverableType::Plugin => "plugin",
431+
}
432+
.into(),
433+
);
434+
table.insert("id", disabled_tool.id.clone().into());
435+
array.push(table);
436+
}
437+
TomlItem::Value(array.into())
438+
}
439+
382440
fn array_from_iter<I>(iter: I) -> TomlItem
383441
where
384442
I: Iterator<Item = String>,
@@ -552,6 +610,9 @@ impl ConfigDocument {
552610
value(*acknowledged),
553611
)),
554612
ConfigEdit::ReplaceMcpServers(servers) => Ok(self.replace_mcp_servers(servers)),
613+
ConfigEdit::AddToolSuggestDisabledTool(disabled_tool) => {
614+
Ok(self.add_tool_suggest_disabled_tool(disabled_tool))
615+
}
555616
ConfigEdit::SetSkillConfig { path, enabled } => {
556617
Ok(self.set_skill_config(SkillConfigSelector::Path(path.clone()), *enabled))
557618
}
@@ -590,6 +651,41 @@ impl ConfigDocument {
590651
self.remove(&resolved)
591652
}
592653

654+
fn add_tool_suggest_disabled_tool(&mut self, disabled_tool: &ToolSuggestDisabledTool) -> bool {
655+
let disabled_tools_item = self
656+
.doc
657+
.get("tool_suggest")
658+
.and_then(|item| item.as_table_like())
659+
.and_then(|table| table.get("disabled_tools"));
660+
let existing_from_array = disabled_tools_item
661+
.and_then(|item| item.as_value())
662+
.and_then(|value| value.as_array())
663+
.into_iter()
664+
.flat_map(|array| array.iter())
665+
.filter_map(document_helpers::parse_tool_suggest_disabled_tool);
666+
let existing_from_tables = disabled_tools_item
667+
.and_then(|item| match item {
668+
TomlItem::ArrayOfTables(array) => Some(array),
669+
_ => None,
670+
})
671+
.into_iter()
672+
.flat_map(|array| array.iter())
673+
.filter_map(document_helpers::parse_tool_suggest_disabled_tool_table);
674+
675+
let mut seen = HashSet::new();
676+
let disabled_tools = existing_from_array
677+
.chain(existing_from_tables)
678+
.chain(std::iter::once(disabled_tool.clone()))
679+
.filter_map(|disabled_tool| disabled_tool.normalized())
680+
.filter(|disabled_tool| seen.insert(disabled_tool.clone()))
681+
.collect::<Vec<_>>();
682+
self.write_value(
683+
Scope::Global,
684+
&["tool_suggest", "disabled_tools"],
685+
document_helpers::tool_suggest_disabled_tools_value(&disabled_tools),
686+
)
687+
}
688+
593689
fn clear_owned(&mut self, segments: &[String]) -> bool {
594690
self.remove(segments)
595691
}

0 commit comments

Comments
 (0)