mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
## Summary - Use the session-loaded plugin app IDs as the source of connector suggestion candidates. - Remove the redundant plugin reload from `tool_suggest_connector_ids()`. - Add regression coverage for connectors declared by a loaded remote plugin, using the Databricks app case. ## Context Loaded remote plugins can declare app connector IDs in `.app.json`. The session-owned `PluginsManager` already loads those plugins and exposes their effective app IDs. The connector suggestion path was creating a separate `PluginsManager` and recomputing plugin app IDs. That new manager does not share the session manager’s remote installed plugin cache, so app IDs from loaded remote plugins were missing from connector suggestions. ## Fix Pass the already-loaded effective app IDs into connector suggestion generation and use them directly as the plugin-derived connector candidate set. Connector candidates are now built from: - App IDs declared by loaded plugins - Explicitly configured connector discoverables - Existing disabled-suggestion filtering This avoids a second plugin-manager lookup and keeps connector suggestions aligned with the plugins actually loaded for the turn. ## Behavior For example, when a plugin is loaded and its `.app.json` declares data apps, `list_available_plugins_to_install` can now return those data connectors. This does not create plugin suggestions from the plugin itself. Plugin suggestions still come from eligible uninstalled entries in the marketplace catalog and require existing matching/filtering rules. ## Validation - `just fmt` - Added regression coverage for a loaded-plugin connector ID appearing in discoverable tools - Attempted `just test -p codex-core`; the command exited unsuccessfully in the local test environment without useful failure detail captured in the run output
712 lines
23 KiB
Rust
712 lines
23 KiB
Rust
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
use std::sync::LazyLock;
|
|
use std::sync::Mutex as StdMutex;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
|
|
use async_channel::unbounded;
|
|
pub use codex_app_server_protocol::AppBranding;
|
|
pub use codex_app_server_protocol::AppInfo;
|
|
pub use codex_app_server_protocol::AppMetadata;
|
|
use codex_connectors::ConnectorDirectoryCacheContext;
|
|
use codex_connectors::ConnectorDirectoryCacheKey;
|
|
use codex_exec_server::EnvironmentManager;
|
|
use codex_exec_server::ExecServerRuntimePaths;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_tools::DiscoverableTool;
|
|
use rmcp::model::ToolAnnotations;
|
|
use serde::Deserialize;
|
|
use tracing::warn;
|
|
|
|
use crate::config::Config;
|
|
use crate::mcp::McpManager;
|
|
use crate::plugins::list_tool_suggest_discoverable_plugins;
|
|
use crate::session::INITIAL_SUBMIT_ID;
|
|
use codex_config::AppsRequirementsToml;
|
|
use codex_config::types::AppToolApproval;
|
|
use codex_config::types::AppsConfigToml;
|
|
use codex_config::types::ToolSuggestDiscoverableType;
|
|
use codex_core_plugins::PluginsManager;
|
|
use codex_features::Feature;
|
|
use codex_login::AuthManager;
|
|
use codex_login::CodexAuth;
|
|
use codex_login::default_client::originator;
|
|
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
|
use codex_mcp::McpConnectionManager;
|
|
use codex_mcp::McpRuntimeContext;
|
|
use codex_mcp::ToolInfo;
|
|
use codex_mcp::ToolPluginProvenance;
|
|
use codex_mcp::codex_apps_tools_cache_key;
|
|
use codex_mcp::compute_auth_statuses;
|
|
use codex_mcp::host_owned_codex_apps_enabled;
|
|
use codex_mcp::with_codex_apps_mcp;
|
|
|
|
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) struct AppToolPolicy {
|
|
pub enabled: bool,
|
|
pub approval: AppToolApproval,
|
|
}
|
|
|
|
impl Default for AppToolPolicy {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
approval: AppToolApproval::Auto,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Eq)]
|
|
struct AccessibleConnectorsCacheKey {
|
|
chatgpt_base_url: String,
|
|
account_id: Option<String>,
|
|
chatgpt_user_id: Option<String>,
|
|
is_workspace_account: bool,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct CachedAccessibleConnectors {
|
|
key: AccessibleConnectorsCacheKey,
|
|
expires_at: Instant,
|
|
connectors: Vec<AppInfo>,
|
|
}
|
|
|
|
static ACCESSIBLE_CONNECTORS_CACHE: LazyLock<StdMutex<Option<CachedAccessibleConnectors>>> =
|
|
LazyLock::new(|| StdMutex::new(None));
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct AccessibleConnectorsStatus {
|
|
pub connectors: Vec<AppInfo>,
|
|
pub codex_apps_ready: bool,
|
|
}
|
|
|
|
pub async fn list_accessible_connectors_from_mcp_tools(
|
|
config: &Config,
|
|
) -> anyhow::Result<Vec<AppInfo>> {
|
|
Ok(
|
|
list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
|
config, /*force_refetch*/ false,
|
|
)
|
|
.await?
|
|
.connectors,
|
|
)
|
|
}
|
|
|
|
pub(crate) async fn list_accessible_and_enabled_connectors_from_manager(
|
|
mcp_connection_manager: &McpConnectionManager,
|
|
config: &Config,
|
|
) -> Vec<AppInfo> {
|
|
with_app_enabled_state(
|
|
accessible_connectors_from_mcp_tools(&mcp_connection_manager.list_all_tools().await),
|
|
config,
|
|
)
|
|
.into_iter()
|
|
.filter(|connector| connector.is_accessible && connector.is_enabled)
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
|
|
config: &Config,
|
|
auth: Option<&CodexAuth>,
|
|
accessible_connectors: &[AppInfo],
|
|
loaded_plugin_app_connector_ids: &[String],
|
|
) -> anyhow::Result<Vec<DiscoverableTool>> {
|
|
let connector_ids = tool_suggest_connector_ids(config, loaded_plugin_app_connector_ids);
|
|
let directory_connectors = codex_connectors::merge::merge_plugin_connectors(
|
|
cached_directory_connectors_for_tool_suggest_with_auth(config, auth).await,
|
|
connector_ids.iter().cloned(),
|
|
);
|
|
let discoverable_connectors =
|
|
codex_connectors::filter::filter_tool_suggest_discoverable_connectors(
|
|
directory_connectors,
|
|
accessible_connectors,
|
|
&connector_ids,
|
|
originator().value.as_str(),
|
|
)
|
|
.into_iter()
|
|
.map(DiscoverableTool::from);
|
|
let discoverable_plugins =
|
|
list_tool_suggest_discoverable_plugins(config, loaded_plugin_app_connector_ids)
|
|
.await?
|
|
.into_iter()
|
|
.map(DiscoverableTool::from);
|
|
Ok(discoverable_connectors
|
|
.chain(discoverable_plugins)
|
|
.collect())
|
|
}
|
|
|
|
pub async fn list_cached_accessible_connectors_from_mcp_tools(
|
|
config: &Config,
|
|
) -> Option<Vec<AppInfo>> {
|
|
let auth_manager =
|
|
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
|
let auth = auth_manager.auth().await;
|
|
if !config
|
|
.features
|
|
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
|
|
{
|
|
return Some(Vec::new());
|
|
}
|
|
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
|
|
read_cached_accessible_connectors(&cache_key).map(|connectors| {
|
|
codex_connectors::filter::filter_disallowed_connectors(
|
|
connectors,
|
|
originator().value.as_str(),
|
|
)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools(
|
|
config: &Config,
|
|
auth: Option<&CodexAuth>,
|
|
mcp_tools: &[ToolInfo],
|
|
) {
|
|
if !config.features.enabled(Feature::Apps) {
|
|
return;
|
|
}
|
|
|
|
let cache_key = accessible_connectors_cache_key(config, auth);
|
|
let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors(
|
|
accessible_connectors_from_mcp_tools(mcp_tools),
|
|
originator().value.as_str(),
|
|
);
|
|
write_cached_accessible_connectors(cache_key, &accessible_connectors);
|
|
}
|
|
|
|
pub async fn list_accessible_connectors_from_mcp_tools_with_options(
|
|
config: &Config,
|
|
force_refetch: bool,
|
|
) -> anyhow::Result<Vec<AppInfo>> {
|
|
Ok(
|
|
list_accessible_connectors_from_mcp_tools_with_options_and_status(config, force_refetch)
|
|
.await?
|
|
.connectors,
|
|
)
|
|
}
|
|
|
|
pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
|
config: &Config,
|
|
force_refetch: bool,
|
|
) -> anyhow::Result<AccessibleConnectorsStatus> {
|
|
// TODO: Wire callers that already own an EnvironmentManager into
|
|
// list_accessible_connectors_from_mcp_tools_with_environment_manager instead
|
|
// of constructing a temporary manager here.
|
|
let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
|
|
config.codex_self_exe.clone(),
|
|
config.codex_linux_sandbox_exe.clone(),
|
|
)?;
|
|
let environment_manager =
|
|
EnvironmentManager::from_codex_home(config.codex_home.clone(), Some(local_runtime_paths))
|
|
.await?;
|
|
list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
|
config,
|
|
force_refetch,
|
|
Arc::new(environment_manager),
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
|
config: &Config,
|
|
force_refetch: bool,
|
|
environment_manager: Arc<EnvironmentManager>,
|
|
) -> anyhow::Result<AccessibleConnectorsStatus> {
|
|
let auth_manager =
|
|
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
|
let auth = auth_manager.auth().await;
|
|
if !config
|
|
.features
|
|
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
|
|
{
|
|
return Ok(AccessibleConnectorsStatus {
|
|
connectors: Vec::new(),
|
|
codex_apps_ready: true,
|
|
});
|
|
}
|
|
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
|
|
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
|
|
let mcp_manager = McpManager::new(Arc::clone(&plugins_manager));
|
|
let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config).await;
|
|
if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key)
|
|
{
|
|
let cached_connectors = codex_connectors::filter::filter_disallowed_connectors(
|
|
cached_connectors,
|
|
originator().value.as_str(),
|
|
);
|
|
let cached_connectors = with_app_plugin_sources(cached_connectors, &tool_plugin_provenance);
|
|
return Ok(AccessibleConnectorsStatus {
|
|
connectors: cached_connectors,
|
|
codex_apps_ready: true,
|
|
});
|
|
}
|
|
|
|
let mcp_config = config.to_mcp_config(plugins_manager.as_ref()).await;
|
|
let mcp_servers = with_codex_apps_mcp(HashMap::new(), auth.as_ref(), &mcp_config);
|
|
let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(&mcp_config, auth.as_ref());
|
|
if mcp_servers.is_empty() {
|
|
return Ok(AccessibleConnectorsStatus {
|
|
connectors: Vec::new(),
|
|
codex_apps_ready: true,
|
|
});
|
|
}
|
|
|
|
let auth_status_entries = compute_auth_statuses(
|
|
mcp_servers.iter(),
|
|
config.mcp_oauth_credentials_store_mode,
|
|
auth.as_ref(),
|
|
)
|
|
.await;
|
|
|
|
let (tx_event, rx_event) = unbounded();
|
|
drop(rx_event);
|
|
|
|
let (mut mcp_connection_manager, cancel_token) = McpConnectionManager::new(
|
|
&mcp_servers,
|
|
config.mcp_oauth_credentials_store_mode,
|
|
auth_status_entries,
|
|
&config.permissions.approval_policy,
|
|
INITIAL_SUBMIT_ID.to_owned(),
|
|
tx_event,
|
|
PermissionProfile::default(),
|
|
// Connector discovery is threadless. Use an actually configured env if
|
|
// one exists, but do not reintroduce the old hidden-local fallback.
|
|
McpRuntimeContext::new(environment_manager, config.cwd.to_path_buf()),
|
|
config.codex_home.to_path_buf(),
|
|
codex_apps_tools_cache_key(auth.as_ref()),
|
|
host_owned_codex_apps_enabled,
|
|
mcp_config.prefix_mcp_tool_names,
|
|
mcp_config.client_elicitation_capability,
|
|
ToolPluginProvenance::default(),
|
|
auth.as_ref(),
|
|
/*elicitation_reviewer*/ None,
|
|
)
|
|
.await;
|
|
|
|
let refreshed_tools = if force_refetch {
|
|
match mcp_connection_manager
|
|
.hard_refresh_codex_apps_tools_cache()
|
|
.await
|
|
{
|
|
Ok(tools) => Some(tools),
|
|
Err(err) => {
|
|
warn!(
|
|
"failed to force-refresh tools for MCP server '{CODEX_APPS_MCP_SERVER_NAME}', using cached/startup tools: {err:#}"
|
|
);
|
|
None
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
let refreshed_tools_succeeded = refreshed_tools.is_some();
|
|
|
|
let mut tools = if let Some(tools) = refreshed_tools {
|
|
tools
|
|
} else {
|
|
mcp_connection_manager.list_all_tools().await
|
|
};
|
|
let mut should_reload_tools = false;
|
|
let codex_apps_ready = if refreshed_tools_succeeded {
|
|
true
|
|
} else if let Some(cfg) = mcp_servers.get(CODEX_APPS_MCP_SERVER_NAME) {
|
|
let immediate_ready = mcp_connection_manager
|
|
.wait_for_server_ready(CODEX_APPS_MCP_SERVER_NAME, Duration::ZERO)
|
|
.await;
|
|
if immediate_ready {
|
|
true
|
|
} else if tools.is_empty() {
|
|
let timeout = cfg
|
|
.configured_config()
|
|
.and_then(|config| config.startup_timeout_sec)
|
|
.unwrap_or(CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS);
|
|
let ready = mcp_connection_manager
|
|
.wait_for_server_ready(CODEX_APPS_MCP_SERVER_NAME, timeout)
|
|
.await;
|
|
should_reload_tools = ready;
|
|
ready
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
};
|
|
if should_reload_tools {
|
|
tools = mcp_connection_manager.list_all_tools().await;
|
|
}
|
|
if codex_apps_ready {
|
|
cancel_token.cancel();
|
|
}
|
|
|
|
let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors(
|
|
accessible_connectors_from_mcp_tools(&tools),
|
|
originator().value.as_str(),
|
|
);
|
|
if codex_apps_ready || !accessible_connectors.is_empty() {
|
|
write_cached_accessible_connectors(cache_key, &accessible_connectors);
|
|
}
|
|
let accessible_connectors =
|
|
with_app_plugin_sources(accessible_connectors, &tool_plugin_provenance);
|
|
mcp_connection_manager.shutdown().await;
|
|
Ok(AccessibleConnectorsStatus {
|
|
connectors: accessible_connectors,
|
|
codex_apps_ready,
|
|
})
|
|
}
|
|
|
|
fn accessible_connectors_cache_key(
|
|
config: &Config,
|
|
auth: Option<&CodexAuth>,
|
|
) -> AccessibleConnectorsCacheKey {
|
|
let account_id = auth.and_then(CodexAuth::get_account_id);
|
|
let chatgpt_user_id = auth.and_then(CodexAuth::get_chatgpt_user_id);
|
|
let is_workspace_account = auth.is_some_and(CodexAuth::is_workspace_account);
|
|
AccessibleConnectorsCacheKey {
|
|
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
|
account_id,
|
|
chatgpt_user_id,
|
|
is_workspace_account,
|
|
}
|
|
}
|
|
|
|
fn read_cached_accessible_connectors(
|
|
cache_key: &AccessibleConnectorsCacheKey,
|
|
) -> Option<Vec<AppInfo>> {
|
|
let mut cache_guard = ACCESSIBLE_CONNECTORS_CACHE
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let now = Instant::now();
|
|
|
|
if let Some(cached) = cache_guard.as_ref() {
|
|
if now < cached.expires_at && cached.key == *cache_key {
|
|
return Some(cached.connectors.clone());
|
|
}
|
|
if now >= cached.expires_at {
|
|
*cache_guard = None;
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn write_cached_accessible_connectors(
|
|
cache_key: AccessibleConnectorsCacheKey,
|
|
connectors: &[AppInfo],
|
|
) {
|
|
let mut cache_guard = ACCESSIBLE_CONNECTORS_CACHE
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
*cache_guard = Some(CachedAccessibleConnectors {
|
|
key: cache_key,
|
|
expires_at: Instant::now() + codex_connectors::CONNECTORS_CACHE_TTL,
|
|
connectors: connectors.to_vec(),
|
|
});
|
|
}
|
|
|
|
fn tool_suggest_connector_ids(
|
|
config: &Config,
|
|
loaded_plugin_app_connector_ids: &[String],
|
|
) -> HashSet<String> {
|
|
let mut connector_ids = loaded_plugin_app_connector_ids
|
|
.iter()
|
|
.cloned()
|
|
.collect::<HashSet<_>>();
|
|
connector_ids.extend(
|
|
config
|
|
.tool_suggest
|
|
.discoverables
|
|
.iter()
|
|
.filter(|discoverable| discoverable.kind == ToolSuggestDiscoverableType::Connector)
|
|
.map(|discoverable| discoverable.id.clone()),
|
|
);
|
|
let disabled_connector_ids = config
|
|
.tool_suggest
|
|
.disabled_tools
|
|
.iter()
|
|
.filter(|disabled_tool| disabled_tool.kind == ToolSuggestDiscoverableType::Connector)
|
|
.map(|disabled_tool| disabled_tool.id.as_str())
|
|
.collect::<HashSet<_>>();
|
|
connector_ids.retain(|connector_id| !disabled_connector_ids.contains(connector_id.as_str()));
|
|
connector_ids
|
|
}
|
|
|
|
async fn cached_directory_connectors_for_tool_suggest_with_auth(
|
|
config: &Config,
|
|
auth: Option<&CodexAuth>,
|
|
) -> Vec<AppInfo> {
|
|
if !config.features.enabled(Feature::Apps) {
|
|
return Vec::new();
|
|
}
|
|
|
|
let loaded_auth;
|
|
let auth = if let Some(auth) = auth {
|
|
Some(auth)
|
|
} else {
|
|
let auth_manager =
|
|
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
|
loaded_auth = auth_manager.auth().await;
|
|
loaded_auth.as_ref()
|
|
};
|
|
let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) else {
|
|
return Vec::new();
|
|
};
|
|
|
|
let account_id = match auth.get_account_id() {
|
|
Some(account_id) if !account_id.is_empty() => account_id,
|
|
_ => return Vec::new(),
|
|
};
|
|
let is_workspace_account = auth.is_workspace_account();
|
|
let cache_context = ConnectorDirectoryCacheContext::new(
|
|
config.codex_home.to_path_buf(),
|
|
ConnectorDirectoryCacheKey::new(
|
|
config.chatgpt_base_url.clone(),
|
|
Some(account_id),
|
|
auth.get_chatgpt_user_id(),
|
|
is_workspace_account,
|
|
),
|
|
);
|
|
|
|
codex_connectors::cached_directory_connectors(&cache_context).unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn accessible_connectors_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Vec<AppInfo> {
|
|
// ToolInfo already carries plugin provenance, so app-level plugin sources
|
|
// can be derived here instead of requiring a separate enrichment pass.
|
|
let tools = mcp_tools.iter().filter_map(|tool| {
|
|
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
|
return None;
|
|
}
|
|
let connector_id = tool.connector_id.as_deref()?;
|
|
Some(codex_connectors::accessible::AccessibleConnectorTool {
|
|
connector_id: connector_id.to_string(),
|
|
connector_name: tool.connector_name.clone(),
|
|
connector_description: tool.namespace_description.clone(),
|
|
plugin_display_names: tool.plugin_display_names.clone(),
|
|
})
|
|
});
|
|
codex_connectors::accessible::collect_accessible_connectors(tools)
|
|
}
|
|
|
|
pub fn with_app_enabled_state(mut connectors: Vec<AppInfo>, config: &Config) -> Vec<AppInfo> {
|
|
let user_apps_config = read_user_apps_config(config);
|
|
let requirements_apps_config = config.config_layer_stack.requirements_toml().apps.as_ref();
|
|
if user_apps_config.is_none() && requirements_apps_config.is_none() {
|
|
return connectors;
|
|
}
|
|
|
|
for connector in &mut connectors {
|
|
if let Some(apps_config) = user_apps_config.as_ref()
|
|
&& (apps_config.default.is_some()
|
|
|| apps_config.apps.contains_key(connector.id.as_str()))
|
|
{
|
|
connector.is_enabled = app_is_enabled(apps_config, Some(connector.id.as_str()));
|
|
}
|
|
|
|
if requirements_apps_config
|
|
.and_then(|apps| apps.apps.get(connector.id.as_str()))
|
|
.is_some_and(|app| app.enabled == Some(false))
|
|
{
|
|
connector.is_enabled = false;
|
|
}
|
|
}
|
|
|
|
connectors
|
|
}
|
|
|
|
pub fn with_app_plugin_sources(
|
|
mut connectors: Vec<AppInfo>,
|
|
tool_plugin_provenance: &ToolPluginProvenance,
|
|
) -> Vec<AppInfo> {
|
|
for connector in &mut connectors {
|
|
connector.plugin_display_names = tool_plugin_provenance
|
|
.plugin_display_names_for_connector_id(connector.id.as_str())
|
|
.to_vec();
|
|
}
|
|
connectors
|
|
}
|
|
|
|
pub(crate) fn app_tool_policy(
|
|
config: &Config,
|
|
connector_id: Option<&str>,
|
|
tool_name: &str,
|
|
tool_title: Option<&str>,
|
|
annotations: Option<&ToolAnnotations>,
|
|
) -> AppToolPolicy {
|
|
let apps_config = read_apps_config(config);
|
|
let managed_approval = managed_app_tool_approval(
|
|
config.config_layer_stack.requirements_toml().apps.as_ref(),
|
|
connector_id,
|
|
tool_name,
|
|
);
|
|
app_tool_policy_from_apps_config(
|
|
apps_config.as_ref(),
|
|
connector_id,
|
|
tool_name,
|
|
tool_title,
|
|
annotations,
|
|
managed_approval,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn codex_app_tool_is_enabled(config: &Config, tool_info: &ToolInfo) -> bool {
|
|
if tool_info.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
|
return true;
|
|
}
|
|
|
|
app_tool_policy(
|
|
config,
|
|
tool_info.connector_id.as_deref(),
|
|
&tool_info.tool.name,
|
|
tool_info.tool.title.as_deref(),
|
|
tool_info.tool.annotations.as_ref(),
|
|
)
|
|
.enabled
|
|
}
|
|
|
|
fn read_apps_config(config: &Config) -> Option<AppsConfigToml> {
|
|
let apps_config = read_user_apps_config(config);
|
|
let had_apps_config = apps_config.is_some();
|
|
let mut apps_config = apps_config.unwrap_or_default();
|
|
apply_requirements_apps_constraints(
|
|
&mut apps_config,
|
|
config.config_layer_stack.requirements_toml().apps.as_ref(),
|
|
);
|
|
if had_apps_config || apps_config.default.is_some() || !apps_config.apps.is_empty() {
|
|
Some(apps_config)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn read_user_apps_config(config: &Config) -> Option<AppsConfigToml> {
|
|
config
|
|
.config_layer_stack
|
|
.effective_config()
|
|
.as_table()
|
|
.and_then(|table| table.get("apps"))
|
|
.cloned()
|
|
.and_then(|value| AppsConfigToml::deserialize(value).ok())
|
|
}
|
|
|
|
fn apply_requirements_apps_constraints(
|
|
apps_config: &mut AppsConfigToml,
|
|
requirements_apps_config: Option<&AppsRequirementsToml>,
|
|
) {
|
|
let Some(requirements_apps_config) = requirements_apps_config else {
|
|
return;
|
|
};
|
|
|
|
for (app_id, requirement) in &requirements_apps_config.apps {
|
|
if requirement.enabled == Some(false) {
|
|
let app = apps_config.apps.entry(app_id.clone()).or_default();
|
|
app.enabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn managed_app_tool_approval(
|
|
requirements_apps_config: Option<&AppsRequirementsToml>,
|
|
connector_id: Option<&str>,
|
|
tool_name: &str,
|
|
) -> Option<AppToolApproval> {
|
|
let connector_id = connector_id?;
|
|
requirements_apps_config?
|
|
.apps
|
|
.get(connector_id)?
|
|
.tools
|
|
.as_ref()?
|
|
.tools
|
|
.get(tool_name)?
|
|
.approval_mode
|
|
}
|
|
|
|
fn app_is_enabled(apps_config: &AppsConfigToml, connector_id: Option<&str>) -> bool {
|
|
let default_enabled = apps_config
|
|
.default
|
|
.as_ref()
|
|
.map(|defaults| defaults.enabled)
|
|
.unwrap_or(true);
|
|
|
|
connector_id
|
|
.and_then(|connector_id| apps_config.apps.get(connector_id))
|
|
.map(|app| app.enabled)
|
|
.unwrap_or(default_enabled)
|
|
}
|
|
|
|
fn app_tool_policy_from_apps_config(
|
|
apps_config: Option<&AppsConfigToml>,
|
|
connector_id: Option<&str>,
|
|
tool_name: &str,
|
|
tool_title: Option<&str>,
|
|
annotations: Option<&ToolAnnotations>,
|
|
managed_approval: Option<AppToolApproval>,
|
|
) -> AppToolPolicy {
|
|
let Some(apps_config) = apps_config else {
|
|
return AppToolPolicy {
|
|
approval: managed_approval.unwrap_or(AppToolApproval::Auto),
|
|
..Default::default()
|
|
};
|
|
};
|
|
|
|
let app = connector_id.and_then(|connector_id| apps_config.apps.get(connector_id));
|
|
let tools = app.and_then(|app| app.tools.as_ref());
|
|
let tool_config = tools.and_then(|tools| {
|
|
tools
|
|
.tools
|
|
.get(tool_name)
|
|
.or_else(|| tool_title.and_then(|title| tools.tools.get(title)))
|
|
});
|
|
let approval = managed_approval
|
|
.or_else(|| tool_config.and_then(|tool| tool.approval_mode))
|
|
.or_else(|| app.and_then(|app| app.default_tools_approval_mode))
|
|
.unwrap_or(AppToolApproval::Auto);
|
|
|
|
if !app_is_enabled(apps_config, connector_id) {
|
|
return AppToolPolicy {
|
|
enabled: false,
|
|
approval,
|
|
};
|
|
}
|
|
|
|
if let Some(enabled) = tool_config.and_then(|tool| tool.enabled) {
|
|
return AppToolPolicy { enabled, approval };
|
|
}
|
|
|
|
if let Some(enabled) = app.and_then(|app| app.default_tools_enabled) {
|
|
return AppToolPolicy { enabled, approval };
|
|
}
|
|
|
|
let app_defaults = apps_config.default.as_ref();
|
|
let destructive_enabled = app
|
|
.and_then(|app| app.destructive_enabled)
|
|
.unwrap_or_else(|| {
|
|
app_defaults
|
|
.map(|defaults| defaults.destructive_enabled)
|
|
.unwrap_or(true)
|
|
});
|
|
let open_world_enabled = app
|
|
.and_then(|app| app.open_world_enabled)
|
|
.unwrap_or_else(|| {
|
|
app_defaults
|
|
.map(|defaults| defaults.open_world_enabled)
|
|
.unwrap_or(true)
|
|
});
|
|
let destructive_hint = annotations
|
|
.and_then(|annotations| annotations.destructive_hint)
|
|
.unwrap_or(true);
|
|
let open_world_hint = annotations
|
|
.and_then(|annotations| annotations.open_world_hint)
|
|
.unwrap_or(true);
|
|
let enabled =
|
|
(destructive_enabled || !destructive_hint) && (open_world_enabled || !open_world_hint);
|
|
|
|
AppToolPolicy { enabled, approval }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "connectors_tests.rs"]
|
|
mod tests;
|