Compare commits

...

1 Commits

Author SHA1 Message Date
Michael Bolin
fc496ed698 codex-tools: extract discoverable tool models 2026-03-30 08:42:23 -07:00
17 changed files with 168 additions and 187 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2613,6 +2613,7 @@ dependencies = [
name = "codex-tools"
version = "0.0.0"
dependencies = [
"codex-app-server-protocol",
"codex-code-mode",
"codex-protocol",
"pretty_assertions",

View File

@@ -121,6 +121,7 @@ use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_rmcp_client::ElicitationResponse;
use codex_rmcp_client::OAuthCredentialsStoreMode;
use codex_terminal_detection::user_agent;
use codex_tools::filter_tool_suggest_discoverable_tools_for_client;
use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_stream_parser::AssistantTextChunk;
use codex_utils_stream_parser::AssistantTextStreamParser;
@@ -6546,7 +6547,7 @@ pub(crate) async fn built_tools(
)
.await
.map(|discoverable_tools| {
crate::tools::discoverable::filter_tool_suggest_discoverable_tools_for_client(
filter_tool_suggest_discoverable_tools_for_client(
discoverable_tools,
turn_context.app_server_client_name.as_deref(),
)

View File

@@ -18,6 +18,7 @@ use codex_connectors::AllConnectorsCacheKey;
use codex_connectors::DirectoryListResponse;
use codex_login::token_data::TokenData;
use codex_protocol::protocol::SandboxPolicy;
use codex_tools::DiscoverableTool;
use rmcp::model::ToolAnnotations;
use serde::Deserialize;
use serde::de::DeserializeOwned;
@@ -44,8 +45,6 @@ use crate::mcp_connection_manager::codex_apps_tools_cache_key;
use crate::plugins::AppConnectorId;
use crate::plugins::PluginsManager;
use crate::plugins::list_tool_suggest_discoverable_plugins;
use crate::tools::discoverable::DiscoverablePluginInfo;
use crate::tools::discoverable::DiscoverableTool;
use codex_features::Feature;
pub use codex_connectors::CONNECTORS_CACHE_TTL;
@@ -133,7 +132,6 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
.map(DiscoverableTool::from);
let discoverable_plugins = list_tool_suggest_discoverable_plugins(config)?
.into_iter()
.map(DiscoverablePluginInfo::from)
.map(DiscoverableTool::from);
Ok(discoverable_connectors
.chain(discoverable_plugins)

View File

@@ -9,6 +9,7 @@ use super::PluginsManager;
use crate::config::Config;
use crate::config::types::ToolSuggestDiscoverableType;
use codex_features::Feature;
use codex_tools::DiscoverablePluginInfo;
const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[
"github@openai-curated",
@@ -23,7 +24,7 @@ const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[
pub(crate) fn list_tool_suggest_discoverable_plugins(
config: &Config,
) -> anyhow::Result<Vec<PluginCapabilitySummary>> {
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
if !config.features.enabled(Feature::Plugins) {
return Ok(Vec::new());
}
@@ -47,7 +48,7 @@ pub(crate) fn list_tool_suggest_discoverable_plugins(
return Ok(Vec::new());
};
let mut discoverable_plugins = Vec::<PluginCapabilitySummary>::new();
let mut discoverable_plugins = Vec::<DiscoverablePluginInfo>::new();
for plugin in curated_marketplace.plugins {
if plugin.installed
|| (!TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str())
@@ -66,14 +67,28 @@ pub(crate) fn list_tool_suggest_discoverable_plugins(
marketplace_path: curated_marketplace.path.clone(),
},
) {
Ok(plugin) => discoverable_plugins.push(plugin.plugin.into()),
Ok(plugin) => {
let plugin: PluginCapabilitySummary = plugin.plugin.into();
discoverable_plugins.push(DiscoverablePluginInfo {
id: plugin.config_name,
name: plugin.display_name,
description: plugin.description,
has_skills: plugin.has_skills,
mcp_server_names: plugin.mcp_server_names,
app_connector_ids: plugin
.app_connector_ids
.into_iter()
.map(|connector_id| connector_id.0)
.collect(),
});
}
Err(err) => warn!("failed to load discoverable plugin suggestion {plugin_id}: {err:#}"),
}
}
discoverable_plugins.sort_by(|left, right| {
left.display_name
.cmp(&right.display_name)
.then_with(|| left.config_name.cmp(&right.config_name))
left.name
.cmp(&right.name)
.then_with(|| left.id.cmp(&right.id))
});
Ok(discoverable_plugins)
}

View File

@@ -5,7 +5,7 @@ use crate::plugins::test_support::write_curated_plugin_sha;
use crate::plugins::test_support::write_file;
use crate::plugins::test_support::write_openai_curated_marketplace;
use crate::plugins::test_support::write_plugins_feature_config;
use crate::tools::discoverable::DiscoverablePluginInfo;
use codex_tools::DiscoverablePluginInfo;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
@@ -18,11 +18,7 @@ async fn list_tool_suggest_discoverable_plugins_returns_uninstalled_curated_plug
write_plugins_feature_config(codex_home.path());
let config = load_plugins_config(codex_home.path()).await;
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config)
.unwrap()
.into_iter()
.map(DiscoverablePluginInfo::from)
.collect::<Vec<_>>();
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap();
assert_eq!(
discoverable_plugins,
@@ -52,11 +48,7 @@ plugins = false
);
let config = load_plugins_config(codex_home.path()).await;
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config)
.unwrap()
.into_iter()
.map(DiscoverablePluginInfo::from)
.collect::<Vec<_>>();
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap();
assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());
}
@@ -76,11 +68,7 @@ async fn list_tool_suggest_discoverable_plugins_normalizes_description() {
);
let config = load_plugins_config(codex_home.path()).await;
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config)
.unwrap()
.into_iter()
.map(DiscoverablePluginInfo::from)
.collect::<Vec<_>>();
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap();
assert_eq!(
discoverable_plugins,
@@ -115,11 +103,7 @@ async fn list_tool_suggest_discoverable_plugins_omits_installed_curated_plugins(
.expect("plugin should install");
let refreshed_config = load_plugins_config(codex_home.path()).await;
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&refreshed_config)
.unwrap()
.into_iter()
.map(DiscoverablePluginInfo::from)
.collect::<Vec<_>>();
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&refreshed_config).unwrap();
assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());
}
@@ -140,11 +124,7 @@ discoverables = [{ type = "plugin", id = "sample@openai-curated" }]
);
let config = load_plugins_config(codex_home.path()).await;
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config)
.unwrap()
.into_iter()
.map(DiscoverablePluginInfo::from)
.collect::<Vec<_>>();
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap();
assert_eq!(
discoverable_plugins,

View File

@@ -1,94 +0,0 @@
use crate::plugins::PluginCapabilitySummary;
use codex_app_server_protocol::AppInfo;
use codex_tools::DiscoverableToolType;
const TUI_CLIENT_NAME: &str = "codex-tui";
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum DiscoverableTool {
Connector(Box<AppInfo>),
Plugin(Box<DiscoverablePluginInfo>),
}
impl DiscoverableTool {
pub(crate) fn tool_type(&self) -> DiscoverableToolType {
match self {
Self::Connector(_) => DiscoverableToolType::Connector,
Self::Plugin(_) => DiscoverableToolType::Plugin,
}
}
pub(crate) fn id(&self) -> &str {
match self {
Self::Connector(connector) => connector.id.as_str(),
Self::Plugin(plugin) => plugin.id.as_str(),
}
}
pub(crate) fn name(&self) -> &str {
match self {
Self::Connector(connector) => connector.name.as_str(),
Self::Plugin(plugin) => plugin.name.as_str(),
}
}
pub(crate) fn install_url(&self) -> Option<&str> {
match self {
Self::Connector(connector) => connector.install_url.as_deref(),
Self::Plugin(_) => None,
}
}
}
impl From<AppInfo> for DiscoverableTool {
fn from(value: AppInfo) -> Self {
Self::Connector(Box::new(value))
}
}
impl From<DiscoverablePluginInfo> for DiscoverableTool {
fn from(value: DiscoverablePluginInfo) -> Self {
Self::Plugin(Box::new(value))
}
}
pub(crate) fn filter_tool_suggest_discoverable_tools_for_client(
discoverable_tools: Vec<DiscoverableTool>,
app_server_client_name: Option<&str>,
) -> Vec<DiscoverableTool> {
if app_server_client_name != Some(TUI_CLIENT_NAME) {
return discoverable_tools;
}
discoverable_tools
.into_iter()
.filter(|tool| !matches!(tool, DiscoverableTool::Plugin(_)))
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct DiscoverablePluginInfo {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) description: Option<String>,
pub(crate) has_skills: bool,
pub(crate) mcp_server_names: Vec<String>,
pub(crate) app_connector_ids: Vec<String>,
}
impl From<PluginCapabilitySummary> for DiscoverablePluginInfo {
fn from(value: PluginCapabilitySummary) -> Self {
Self {
id: value.config_name,
name: value.display_name,
description: value.description,
has_skills: value.has_skills,
mcp_server_names: value.mcp_server_names,
app_connector_ids: value
.app_connector_ids
.into_iter()
.map(|connector_id| connector_id.0)
.collect(),
}
}
}

View File

@@ -8,8 +8,10 @@ use codex_app_server_protocol::McpElicitationSchema;
use codex_app_server_protocol::McpServerElicitationRequest;
use codex_app_server_protocol::McpServerElicitationRequestParams;
use codex_rmcp_client::ElicitationAction;
use codex_tools::DiscoverableTool;
use codex_tools::DiscoverableToolAction;
use codex_tools::DiscoverableToolType;
use codex_tools::filter_tool_suggest_discoverable_tools_for_client;
use rmcp::model::RequestId;
use serde::Deserialize;
use serde::Serialize;
@@ -22,8 +24,6 @@ use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::discoverable::DiscoverableTool;
use crate::tools::discoverable::filter_tool_suggest_discoverable_tools_for_client;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;

View File

@@ -5,9 +5,9 @@ use crate::plugins::test_support::load_plugins_config;
use crate::plugins::test_support::write_curated_plugin_sha;
use crate::plugins::test_support::write_openai_curated_marketplace;
use crate::plugins::test_support::write_plugins_feature_config;
use crate::tools::discoverable::DiscoverablePluginInfo;
use crate::tools::discoverable::filter_tool_suggest_discoverable_tools_for_client;
use codex_app_server_protocol::AppInfo;
use codex_tools::DiscoverablePluginInfo;
use codex_tools::DiscoverableTool;
use codex_tools::DiscoverableToolAction;
use codex_tools::DiscoverableToolType;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -161,54 +161,6 @@ fn build_tool_suggestion_meta_uses_expected_shape() {
);
}
#[test]
fn filter_tool_suggest_discoverable_tools_for_codex_tui_omits_plugins() {
let discoverable_tools = vec![
DiscoverableTool::Connector(Box::new(AppInfo {
id: "connector_google_calendar".to_string(),
name: "Google Calendar".to_string(),
description: Some("Plan events and schedules.".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: Some("https://example.test/google-calendar".to_string()),
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
})),
DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
id: "slack@openai-curated".to_string(),
name: "Slack".to_string(),
description: Some("Search Slack messages".to_string()),
has_skills: true,
mcp_server_names: vec!["slack".to_string()],
app_connector_ids: vec!["connector_slack".to_string()],
})),
];
assert_eq!(
filter_tool_suggest_discoverable_tools_for_client(discoverable_tools, Some("codex-tui"),),
vec![DiscoverableTool::Connector(Box::new(AppInfo {
id: "connector_google_calendar".to_string(),
name: "Google Calendar".to_string(),
description: Some("Plan events and schedules.".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: Some("https://example.test/google-calendar".to_string()),
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
}))]
);
}
#[test]
fn verified_connector_suggestion_completed_requires_accessible_connector() {
let accessible_connectors = vec![AppInfo {

View File

@@ -1,6 +1,5 @@
pub mod code_mode;
pub mod context;
pub(crate) mod discoverable;
pub mod events;
pub(crate) mod handlers;
pub mod js_repl;

View File

@@ -7,7 +7,6 @@ use crate::sandboxing::SandboxPermissions;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::discoverable::DiscoverableTool;
use crate::tools::registry::AnyToolResult;
use crate::tools::registry::ToolRegistry;
use crate::tools::spec::ToolsConfig;
@@ -18,6 +17,7 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::models::SearchToolCallParams;
use codex_protocol::models::ShellToolCallParams;
use codex_tools::ConfiguredToolSpec;
use codex_tools::DiscoverableTool;
use rmcp::model::Tool;
use std::collections::HashMap;
use std::sync::Arc;

View File

@@ -7,7 +7,6 @@ use crate::shell::Shell;
use crate::shell::ShellType;
use crate::tools::code_mode::PUBLIC_TOOL_NAME;
use crate::tools::code_mode::WAIT_TOOL_NAME;
use crate::tools::discoverable::DiscoverableTool;
use crate::tools::handlers::PLAN_TOOL;
use crate::tools::handlers::TOOL_SEARCH_DEFAULT_LIMIT;
use crate::tools::handlers::TOOL_SEARCH_TOOL_NAME;
@@ -38,6 +37,7 @@ use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_tools::CommandToolOptions;
use codex_tools::DiscoverableTool;
use codex_tools::DiscoverableToolType;
use codex_tools::ShellToolOptions;
use codex_tools::SpawnAgentToolOptions;

View File

@@ -4,7 +4,6 @@ use crate::models_manager::model_info::with_config_overrides;
use crate::shell::Shell;
use crate::shell::ShellType;
use crate::tools::ToolRouter;
use crate::tools::discoverable::DiscoverablePluginInfo;
use crate::tools::router::ToolRouterParams;
use codex_app_server_protocol::AppInfo;
use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
@@ -14,6 +13,8 @@ use codex_protocol::openai_models::ModelsResponse;
use codex_tools::AdditionalProperties;
use codex_tools::CommandToolOptions;
use codex_tools::ConfiguredToolSpec;
use codex_tools::DiscoverablePluginInfo;
use codex_tools::DiscoverableTool;
use codex_tools::FreeformTool;
use codex_tools::ResponsesApiTool;
use codex_tools::ResponsesApiWebSearchFilters;

View File

@@ -8,6 +8,7 @@ version.workspace = true
workspace = true
[dependencies]
codex-app-server-protocol = { workspace = true }
codex-code-mode = { workspace = true }
codex-protocol = { workspace = true }
rmcp = { workspace = true, default-features = false, features = [

View File

@@ -27,7 +27,7 @@ schema and Responses API tool primitives that no longer need to live in
- local host tool spec builders for shell/exec/request-permissions/view-image
- collaboration and agent-job `ToolSpec` builders for spawn/send/wait/close,
`request_user_input`, and CSV fanout/reporting
- tool discovery and suggestion models / `ToolSpec` builders for
- discoverable-tool models, client filtering, and `ToolSpec` builders for
`tool_search` and `tool_suggest`
- `parse_tool_input_schema()`
- `parse_dynamic_tool()`

View File

@@ -67,12 +67,15 @@ pub use responses_api::mcp_tool_to_deferred_responses_api_tool;
pub use responses_api::mcp_tool_to_responses_api_tool;
pub use responses_api::tool_definition_to_responses_api_tool;
pub use tool_definition::ToolDefinition;
pub use tool_discovery::DiscoverablePluginInfo;
pub use tool_discovery::DiscoverableTool;
pub use tool_discovery::DiscoverableToolAction;
pub use tool_discovery::DiscoverableToolType;
pub use tool_discovery::ToolSearchAppInfo;
pub use tool_discovery::ToolSuggestEntry;
pub use tool_discovery::create_tool_search_tool;
pub use tool_discovery::create_tool_suggest_tool;
pub use tool_discovery::filter_tool_suggest_discoverable_tools_for_client;
pub use tool_spec::ConfiguredToolSpec;
pub use tool_spec::ResponsesApiWebSearchFilters;
pub use tool_spec::ResponsesApiWebSearchUserLocation;

View File

@@ -1,10 +1,13 @@
use crate::JsonSchema;
use crate::ResponsesApiTool;
use crate::ToolSpec;
use codex_app_server_protocol::AppInfo;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
const TUI_CLIENT_NAME: &str = "codex-tui";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolSearchAppInfo {
pub name: String,
@@ -34,6 +37,78 @@ pub enum DiscoverableToolAction {
Enable,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DiscoverableTool {
Connector(Box<AppInfo>),
Plugin(Box<DiscoverablePluginInfo>),
}
impl DiscoverableTool {
pub fn tool_type(&self) -> DiscoverableToolType {
match self {
Self::Connector(_) => DiscoverableToolType::Connector,
Self::Plugin(_) => DiscoverableToolType::Plugin,
}
}
pub fn id(&self) -> &str {
match self {
Self::Connector(connector) => connector.id.as_str(),
Self::Plugin(plugin) => plugin.id.as_str(),
}
}
pub fn name(&self) -> &str {
match self {
Self::Connector(connector) => connector.name.as_str(),
Self::Plugin(plugin) => plugin.name.as_str(),
}
}
pub fn install_url(&self) -> Option<&str> {
match self {
Self::Connector(connector) => connector.install_url.as_deref(),
Self::Plugin(_) => None,
}
}
}
impl From<AppInfo> for DiscoverableTool {
fn from(value: AppInfo) -> Self {
Self::Connector(Box::new(value))
}
}
impl From<DiscoverablePluginInfo> for DiscoverableTool {
fn from(value: DiscoverablePluginInfo) -> Self {
Self::Plugin(Box::new(value))
}
}
pub fn filter_tool_suggest_discoverable_tools_for_client(
discoverable_tools: Vec<DiscoverableTool>,
app_server_client_name: Option<&str>,
) -> Vec<DiscoverableTool> {
if app_server_client_name != Some(TUI_CLIENT_NAME) {
return discoverable_tools;
}
discoverable_tools
.into_iter()
.filter(|tool| !matches!(tool, DiscoverableTool::Plugin(_)))
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiscoverablePluginInfo {
pub id: String,
pub name: String,
pub description: Option<String>,
pub has_skills: bool,
pub mcp_server_names: Vec<String>,
pub app_connector_ids: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolSuggestEntry {
pub id: String,

View File

@@ -1,4 +1,5 @@
use super::*;
use codex_app_server_protocol::AppInfo;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
@@ -147,3 +148,51 @@ fn discoverable_tool_enums_use_expected_wire_names() {
})
);
}
#[test]
fn filter_tool_suggest_discoverable_tools_for_codex_tui_omits_plugins() {
let discoverable_tools = vec![
DiscoverableTool::Connector(Box::new(AppInfo {
id: "connector_google_calendar".to_string(),
name: "Google Calendar".to_string(),
description: Some("Plan events and schedules.".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: Some("https://example.test/google-calendar".to_string()),
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
})),
DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
id: "slack@openai-curated".to_string(),
name: "Slack".to_string(),
description: Some("Search Slack messages".to_string()),
has_skills: true,
mcp_server_names: vec!["slack".to_string()],
app_connector_ids: vec!["connector_slack".to_string()],
})),
];
assert_eq!(
filter_tool_suggest_discoverable_tools_for_client(discoverable_tools, Some("codex-tui"),),
vec![DiscoverableTool::Connector(Box::new(AppInfo {
id: "connector_google_calendar".to_string(),
name: "Google Calendar".to_string(),
description: Some("Plan events and schedules.".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: Some("https://example.test/google-calendar".to_string()),
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
}))]
);
}