[plugins] Support configuration tool suggest allowlist. (#15022)
- [x] Support configuration tool suggest allowlist. Supports both plugins and connectors.
This commit is contained in:
parent
84f4e7b39d
commit
40a7d1d15b
8 changed files with 252 additions and 6 deletions
|
|
@ -1549,6 +1549,42 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSuggestConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"discoverables": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/ToolSuggestDiscoverable"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSuggestDiscoverable": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/ToolSuggestDiscoverableType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSuggestDiscoverableType": {
|
||||
"enum": [
|
||||
"connector",
|
||||
"plugin"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ToolsToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
|
@ -2431,6 +2467,14 @@
|
|||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"tool_suggest": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolSuggestConfig"
|
||||
}
|
||||
],
|
||||
"description": "Additional discoverable tools that can be suggested for installation."
|
||||
},
|
||||
"tools": {
|
||||
"allOf": [
|
||||
{
|
||||
|
|
@ -2479,4 +2523,4 @@
|
|||
},
|
||||
"title": "ConfigToml",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ use crate::config::types::MemoriesToml;
|
|||
use crate::config::types::ModelAvailabilityNuxConfig;
|
||||
use crate::config::types::NotificationMethod;
|
||||
use crate::config::types::Notifications;
|
||||
use crate::config::types::ToolSuggestDiscoverableType;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::features::Feature;
|
||||
use assert_matches::assert_matches;
|
||||
|
|
@ -4344,6 +4345,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
|||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
analytics_enabled: Some(true),
|
||||
feedback_enabled: true,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
tui_theme: None,
|
||||
|
|
@ -4484,6 +4486,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
|||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
analytics_enabled: Some(true),
|
||||
feedback_enabled: true,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
tui_theme: None,
|
||||
|
|
@ -4622,6 +4625,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
|||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
analytics_enabled: Some(false),
|
||||
feedback_enabled: true,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
tui_theme: None,
|
||||
|
|
@ -4746,6 +4750,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
|||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
analytics_enabled: Some(true),
|
||||
feedback_enabled: true,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
tui_theme: None,
|
||||
|
|
@ -5882,6 +5887,65 @@ async fn feature_requirements_reject_collab_legacy_alias() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggest_discoverables_load_from_config_toml() -> std::io::Result<()> {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
[tool_suggest]
|
||||
discoverables = [
|
||||
{ type = "connector", id = "connector_alpha" },
|
||||
{ type = "plugin", id = "plugin_alpha@openai-curated" },
|
||||
{ type = "connector", id = " " }
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed");
|
||||
|
||||
assert_eq!(
|
||||
cfg.tool_suggest,
|
||||
Some(ToolSuggestConfig {
|
||||
discoverables: vec![
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Connector,
|
||||
id: "connector_alpha".to_string(),
|
||||
},
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Plugin,
|
||||
id: "plugin_alpha@openai-curated".to_string(),
|
||||
},
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Connector,
|
||||
id: " ".to_string(),
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
config.tool_suggest,
|
||||
ToolSuggestConfig {
|
||||
discoverables: vec![
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Connector,
|
||||
id: "connector_alpha".to_string(),
|
||||
},
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Plugin,
|
||||
id: "plugin_alpha@openai-curated".to_string(),
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn experimental_realtime_start_instructions_load_from_config_toml() -> std::io::Result<()> {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ use crate::config::types::SandboxWorkspaceWrite;
|
|||
use crate::config::types::ShellEnvironmentPolicy;
|
||||
use crate::config::types::ShellEnvironmentPolicyToml;
|
||||
use crate::config::types::SkillsConfig;
|
||||
use crate::config::types::ToolSuggestConfig;
|
||||
use crate::config::types::ToolSuggestDiscoverable;
|
||||
use crate::config::types::Tui;
|
||||
use crate::config::types::UriBasedFileOpener;
|
||||
use crate::config::types::WindowsSandboxModeToml;
|
||||
|
|
@ -581,6 +583,9 @@ pub struct Config {
|
|||
/// Defaults to `true`.
|
||||
pub feedback_enabled: bool,
|
||||
|
||||
/// Configured discoverable tools for tool suggestions.
|
||||
pub tool_suggest: ToolSuggestConfig,
|
||||
|
||||
/// OTEL configuration (exporter type, endpoint, headers, etc.).
|
||||
pub otel: crate::config::types::OtelConfig,
|
||||
}
|
||||
|
|
@ -1424,6 +1429,9 @@ pub struct ConfigToml {
|
|||
/// Nested tools section for feature toggles
|
||||
pub tools: Option<ToolsToml>,
|
||||
|
||||
/// Additional discoverable tools that can be suggested for installation.
|
||||
pub tool_suggest: Option<ToolSuggestConfig>,
|
||||
|
||||
/// Agent-related settings (thread limits, etc.).
|
||||
pub agents: Option<AgentsToml>,
|
||||
|
||||
|
|
@ -1621,6 +1629,28 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
fn resolve_tool_suggest_config(config_toml: &ConfigToml) -> ToolSuggestConfig {
|
||||
let discoverables = config_toml
|
||||
.tool_suggest
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|tool_suggest| tool_suggest.discoverables.iter())
|
||||
.filter_map(|discoverable| {
|
||||
let trimmed = discoverable.id.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ToolSuggestDiscoverable {
|
||||
kind: discoverable.kind,
|
||||
id: trimmed.to_string(),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
ToolSuggestConfig { discoverables }
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentsToml {
|
||||
|
|
@ -2140,6 +2170,7 @@ impl Config {
|
|||
.clone(),
|
||||
None => ConfigProfile::default(),
|
||||
};
|
||||
let tool_suggest = resolve_tool_suggest_config(&cfg);
|
||||
let feature_overrides = FeatureOverrides {
|
||||
include_apply_patch_tool: include_apply_patch_tool_override,
|
||||
web_search_request: override_tools_web_search_request,
|
||||
|
|
@ -2618,7 +2649,6 @@ impl Config {
|
|||
} else {
|
||||
NetworkSandboxPolicy::from(&effective_sandbox_policy)
|
||||
};
|
||||
|
||||
let config = Self {
|
||||
model,
|
||||
service_tier,
|
||||
|
|
@ -2760,6 +2790,7 @@ impl Config {
|
|||
.as_ref()
|
||||
.and_then(|feedback| feedback.enabled)
|
||||
.unwrap_or(true),
|
||||
tool_suggest,
|
||||
tui_notifications: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -372,6 +372,28 @@ pub struct FeedbackConfigToml {
|
|||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ToolSuggestDiscoverableType {
|
||||
Connector,
|
||||
Plugin,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ToolSuggestDiscoverable {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: ToolSuggestDiscoverableType,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ToolSuggestConfig {
|
||||
#[serde(default)]
|
||||
pub discoverables: Vec<ToolSuggestDiscoverable>,
|
||||
}
|
||||
|
||||
/// Memories settings loaded from config.toml.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ use crate::SandboxState;
|
|||
use crate::config::Config;
|
||||
use crate::config::types::AppToolApproval;
|
||||
use crate::config::types::AppsConfigToml;
|
||||
use crate::config::types::ToolSuggestDiscoverableType;
|
||||
use crate::config_loader::AppsRequirementsToml;
|
||||
use crate::default_client::create_client;
|
||||
use crate::default_client::is_first_party_chat_originator;
|
||||
|
|
@ -376,13 +377,22 @@ fn filter_tool_suggest_discoverable_connectors(
|
|||
}
|
||||
|
||||
fn tool_suggest_connector_ids(config: &Config) -> HashSet<String> {
|
||||
PluginsManager::new(config.codex_home.clone())
|
||||
let mut connector_ids = PluginsManager::new(config.codex_home.clone())
|
||||
.plugins_for_config(config)
|
||||
.capability_summaries()
|
||||
.iter()
|
||||
.flat_map(|plugin| plugin.app_connector_ids.iter())
|
||||
.map(|connector_id| connector_id.0.clone())
|
||||
.collect()
|
||||
.collect::<HashSet<_>>();
|
||||
connector_ids.extend(
|
||||
config
|
||||
.tool_suggest
|
||||
.discoverables
|
||||
.iter()
|
||||
.filter(|discoverable| discoverable.kind == ToolSuggestDiscoverableType::Connector)
|
||||
.map(|discoverable| discoverable.id.clone()),
|
||||
);
|
||||
connector_ids
|
||||
}
|
||||
|
||||
async fn list_directory_connectors_for_tool_suggest_with_auth(
|
||||
|
|
|
|||
|
|
@ -980,6 +980,33 @@ fn first_party_chat_originator_filters_target_and_openai_prefixed_connectors() {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_suggest_connector_ids_include_configured_tool_suggest_discoverables() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[tool_suggest]
|
||||
discoverables = [
|
||||
{ type = "connector", id = "connector_2128aebfecb84f64a069897515042a44" },
|
||||
{ type = "plugin", id = "slack@openai-curated" },
|
||||
{ type = "connector", id = " " }
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.expect("write config");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("config should load");
|
||||
|
||||
assert_eq!(
|
||||
tool_suggest_connector_ids(&config),
|
||||
HashSet::from(["connector_2128aebfecb84f64a069897515042a44".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstalled_apps() {
|
||||
let filtered = filter_tool_suggest_discoverable_connectors(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::Context;
|
||||
use std::collections::HashSet;
|
||||
use tracing::warn;
|
||||
|
||||
use super::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
|
|
@ -6,6 +7,7 @@ use super::PluginCapabilitySummary;
|
|||
use super::PluginReadRequest;
|
||||
use super::PluginsManager;
|
||||
use crate::config::Config;
|
||||
use crate::config::types::ToolSuggestDiscoverableType;
|
||||
use crate::features::Feature;
|
||||
|
||||
const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[
|
||||
|
|
@ -28,6 +30,13 @@ pub(crate) fn list_tool_suggest_discoverable_plugins(
|
|||
}
|
||||
|
||||
let plugins_manager = PluginsManager::new(config.codex_home.clone());
|
||||
let configured_plugin_ids = config
|
||||
.tool_suggest
|
||||
.discoverables
|
||||
.iter()
|
||||
.filter(|discoverable| discoverable.kind == ToolSuggestDiscoverableType::Plugin)
|
||||
.map(|discoverable| discoverable.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let marketplaces = plugins_manager
|
||||
.list_marketplaces_for_config(config, &[])
|
||||
.context("failed to list plugin marketplaces for tool suggestions")?;
|
||||
|
|
@ -41,10 +50,12 @@ pub(crate) fn list_tool_suggest_discoverable_plugins(
|
|||
let mut discoverable_plugins = Vec::<PluginCapabilitySummary>::new();
|
||||
for plugin in curated_marketplace.plugins {
|
||||
if plugin.installed
|
||||
|| !TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str())
|
||||
|| (!TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str())
|
||||
&& !configured_plugin_ids.contains(plugin.id.as_str()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let plugin_id = plugin.id.clone();
|
||||
let plugin_name = plugin.name.clone();
|
||||
|
||||
|
|
@ -56,7 +67,7 @@ pub(crate) fn list_tool_suggest_discoverable_plugins(
|
|||
},
|
||||
) {
|
||||
Ok(plugin) => discoverable_plugins.push(plugin.plugin.into()),
|
||||
Err(err) => warn!("failed to load curated plugin suggestion {plugin_id}: {err:#}"),
|
||||
Err(err) => warn!("failed to load discoverable plugin suggestion {plugin_id}: {err:#}"),
|
||||
}
|
||||
}
|
||||
discoverable_plugins.sort_by(|left, right| {
|
||||
|
|
|
|||
|
|
@ -117,3 +117,40 @@ async fn list_tool_suggest_discoverable_plugins_omits_installed_curated_plugins(
|
|||
|
||||
assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_tool_suggest_discoverable_plugins_includes_configured_plugin_ids() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
let curated_root = crate::plugins::curated_plugins_repo_path(codex_home.path());
|
||||
write_openai_curated_marketplace(&curated_root, &["sample"]);
|
||||
write_file(
|
||||
&codex_home.path().join(crate::config::CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[tool_suggest]
|
||||
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<_>>();
|
||||
|
||||
assert_eq!(
|
||||
discoverable_plugins,
|
||||
vec![DiscoverablePluginInfo {
|
||||
id: "sample@openai-curated".to_string(),
|
||||
name: "sample".to_string(),
|
||||
description: Some(
|
||||
"Plugin that includes skills, MCP servers, and app connectors".to_string(),
|
||||
),
|
||||
has_skills: true,
|
||||
mcp_server_names: vec!["sample-docs".to_string()],
|
||||
app_connector_ids: vec!["connector_calendar".to_string()],
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue