mirror of
https://github.com/openai/codex.git
synced 2026-10-25 07:55:34 +00:00
## What changed - Add environment-provided MCP restrictions for configured and plugin-provided servers. - Disable attachment-scoped servers while their environment configuration is pending, failed, or unselected, while preserving selected-plugin access and the controller-owned Apps server. - Apply the resolved policy to runtime startup, model tool exposure, telemetry, OAuth flows, and skill dependency installation. ## Testing - Cover policy filtering for configured and plugin servers, environment state transitions, Apps ownership, and managed OAuth rejection. GitOrigin-RevId: 7ca5d4dda14068e758497f88835d5cba685e159e
351 lines
14 KiB
Rust
351 lines
14 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use crate::config::Config;
|
|
use crate::environment_selection::ThreadEnvironments;
|
|
use codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
|
use codex_config::McpServerConfig;
|
|
use codex_connectors::ConnectorRuntimeManager;
|
|
use codex_connectors::ConnectorSnapshot;
|
|
use codex_connectors::PluginConnectorSource;
|
|
use codex_core_plugins::PluginsManager;
|
|
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
|
|
use codex_extension_api::ExtensionData;
|
|
use codex_extension_api::ExtensionDataInit;
|
|
use codex_extension_api::ExtensionRegistry;
|
|
use codex_extension_api::McpServerContribution;
|
|
use codex_extension_api::McpServerContributionContext;
|
|
use codex_extension_api::SelectedPluginIdentity;
|
|
use codex_extension_api::SelectedPluginSnapshot;
|
|
use codex_login::CodexAuth;
|
|
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
|
use codex_mcp::EffectiveMcpServer;
|
|
use codex_mcp::McpConfig;
|
|
use codex_mcp::McpEnvironmentAuthority;
|
|
use codex_mcp::McpPluginAttribution;
|
|
use codex_mcp::McpServerRegistration;
|
|
use codex_mcp::McpToolCatalogCache;
|
|
use codex_mcp::ToolInfo;
|
|
use codex_mcp::codex_apps_mcp_server_config;
|
|
use codex_mcp::configured_mcp_servers;
|
|
use codex_mcp::effective_mcp_servers;
|
|
use codex_plugin::AppConnectorId;
|
|
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
|
use codex_protocol::protocol::EnvironmentConfigState;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
|
|
|
const LEGACY_CODEX_APPS_REGISTRATION_ID: &str = "legacy_codex_apps";
|
|
|
|
/// MCP configuration and capability availability derived from the same inputs.
|
|
#[derive(Clone)]
|
|
pub(crate) struct McpRuntimeProjection {
|
|
pub(crate) config: McpConfig,
|
|
pub(crate) plugins_available: bool,
|
|
pub(crate) selected_plugins: SelectedPluginSnapshot,
|
|
}
|
|
|
|
pub(crate) enum McpEnvironmentScope<'a> {
|
|
/// Controller-level operations without an associated thread.
|
|
HostOnly,
|
|
/// Initial thread selections before the live environment store exists.
|
|
Initial(&'a [TurnEnvironmentSelection]),
|
|
/// Current attachment state for an existing thread.
|
|
Live(&'a ThreadEnvironments),
|
|
}
|
|
|
|
pub(crate) struct McpThreadIdentity<'a> {
|
|
pub(crate) session_source: &'a SessionSource,
|
|
pub(crate) originator: &'a str,
|
|
pub(crate) environments: McpEnvironmentScope<'a>,
|
|
}
|
|
|
|
enum OrderedMcpOverlay {
|
|
Set(Box<McpServerRegistration>),
|
|
Remove {
|
|
contributor_id: &'static str,
|
|
contribution_order: usize,
|
|
name: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct McpManager {
|
|
plugins_manager: Arc<PluginsManager>,
|
|
extensions: Arc<ExtensionRegistry<Config>>,
|
|
codex_apps_tools_cache: ConnectorRuntimeManager<ToolInfo>,
|
|
tool_catalog_cache: McpToolCatalogCache,
|
|
}
|
|
|
|
impl McpManager {
|
|
pub fn new(plugins_manager: Arc<PluginsManager>) -> Self {
|
|
Self::new_with_extensions(
|
|
plugins_manager,
|
|
codex_extension_api::empty_extension_registry(),
|
|
ConnectorRuntimeManager::default(),
|
|
)
|
|
}
|
|
|
|
/// Creates a manager that resolves host-installed MCP contributions.
|
|
pub fn new_with_extensions(
|
|
plugins_manager: Arc<PluginsManager>,
|
|
extensions: Arc<ExtensionRegistry<Config>>,
|
|
codex_apps_tools_cache: ConnectorRuntimeManager<ToolInfo>,
|
|
) -> Self {
|
|
Self {
|
|
plugins_manager,
|
|
extensions,
|
|
codex_apps_tools_cache,
|
|
tool_catalog_cache: McpToolCatalogCache::default(),
|
|
}
|
|
}
|
|
|
|
pub fn codex_apps_tools_cache(&self) -> ConnectorRuntimeManager<ToolInfo> {
|
|
self.codex_apps_tools_cache.clone()
|
|
}
|
|
|
|
pub fn tool_catalog_cache(&self) -> McpToolCatalogCache {
|
|
self.tool_catalog_cache.clone()
|
|
}
|
|
|
|
/// Returns the MCP config after applying compatibility built-ins and
|
|
/// runtime-only extension overlays.
|
|
pub async fn runtime_config(&self, config: &Config) -> McpConfig {
|
|
self.runtime_config_with_context(
|
|
McpServerContributionContext::global(config),
|
|
// Threadless discovery and control-plane paths have no effective thread
|
|
// originator; active-thread tool calls use runtime_config_for_step below.
|
|
/*originator*/
|
|
None,
|
|
McpEnvironmentScope::HostOnly,
|
|
)
|
|
.await
|
|
.config
|
|
}
|
|
|
|
#[tracing::instrument(name = "mcp.runtime_config.project_for_step", skip_all)]
|
|
pub(crate) async fn runtime_config_for_step(
|
|
&self,
|
|
config: &Config,
|
|
thread_init: &ExtensionDataInit,
|
|
thread_store: &ExtensionData,
|
|
identity: McpThreadIdentity<'_>,
|
|
ready_selected_capability_roots: &[SelectedCapabilityRoot],
|
|
executor_capability_discovery: Option<&ExecutorCapabilityDiscoverySnapshot>,
|
|
) -> McpRuntimeProjection {
|
|
self.runtime_config_with_context(
|
|
McpServerContributionContext::for_step(
|
|
config,
|
|
thread_init,
|
|
thread_store,
|
|
identity.originator,
|
|
ready_selected_capability_roots,
|
|
executor_capability_discovery,
|
|
)
|
|
.with_session_source(identity.session_source),
|
|
Some(identity.originator),
|
|
identity.environments,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn runtime_config_with_context(
|
|
&self,
|
|
context: McpServerContributionContext<'_, Config>,
|
|
originator: Option<&str>,
|
|
environment_scope: McpEnvironmentScope<'_>,
|
|
) -> McpRuntimeProjection {
|
|
let config = context.config();
|
|
let mut selected_plugin_available = false;
|
|
let mut selected_plugin_connector_sources = Vec::new();
|
|
let mut selected_plugin_registrations = Vec::new();
|
|
let mut selected_plugins = Vec::new();
|
|
let mut overlays = Vec::new();
|
|
// A contributor can emit multiple ordered actions, so order each action globally rather
|
|
// than enumerating contributors.
|
|
let mut contribution_order = 0;
|
|
for contributor in self.extensions.mcp_server_contributors() {
|
|
for contribution in contributor.contribute(context).await {
|
|
match contribution {
|
|
McpServerContribution::Set { name, config } => {
|
|
overlays.push(OrderedMcpOverlay::Set(Box::new(
|
|
McpServerRegistration::from_extension(
|
|
name,
|
|
contributor.id(),
|
|
contribution_order,
|
|
*config,
|
|
),
|
|
)));
|
|
}
|
|
McpServerContribution::HostedApps { config } => {
|
|
overlays.push(OrderedMcpOverlay::Set(Box::new(
|
|
McpServerRegistration::from_hosted_apps(
|
|
contributor.id(),
|
|
contribution_order,
|
|
*config,
|
|
),
|
|
)));
|
|
}
|
|
McpServerContribution::SelectedPlugin {
|
|
name,
|
|
plugin_id,
|
|
plugin_display_name,
|
|
selection_order,
|
|
config,
|
|
} => selected_plugin_registrations.push(
|
|
McpServerRegistration::from_selected_plugin(
|
|
name,
|
|
McpPluginAttribution::new(plugin_id, plugin_display_name),
|
|
selection_order,
|
|
*config,
|
|
),
|
|
),
|
|
McpServerContribution::SelectedPluginPackage {
|
|
selected_root_id,
|
|
plugin_id,
|
|
plugin_display_name,
|
|
connector_ids,
|
|
} => {
|
|
selected_plugin_available = true;
|
|
selected_plugins.push(SelectedPluginIdentity {
|
|
selected_root_id,
|
|
plugin_id: plugin_id.clone(),
|
|
});
|
|
if !connector_ids.is_empty() {
|
|
selected_plugin_connector_sources.push(
|
|
PluginConnectorSource::from_connector_ids(
|
|
plugin_id,
|
|
plugin_display_name,
|
|
connector_ids.into_iter().map(AppConnectorId),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
McpServerContribution::Remove { name } => {
|
|
overlays.push(OrderedMcpOverlay::Remove {
|
|
contributor_id: contributor.id(),
|
|
contribution_order,
|
|
name,
|
|
});
|
|
}
|
|
}
|
|
contribution_order += 1;
|
|
}
|
|
}
|
|
|
|
let loaded_plugins = self
|
|
.plugins_manager
|
|
.plugins_for_config(&config.plugins_config_input())
|
|
.await;
|
|
let plugins_available =
|
|
selected_plugin_available || !loaded_plugins.capability_summaries().is_empty();
|
|
let mut mcp_config = config
|
|
.to_mcp_config_with_loaded_plugins(&loaded_plugins, selected_plugin_registrations);
|
|
let mut catalog = mcp_config.mcp_server_catalog.to_builder();
|
|
if mcp_config.apps_enabled {
|
|
catalog.register(McpServerRegistration::from_compatibility(
|
|
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
LEGACY_CODEX_APPS_REGISTRATION_ID,
|
|
codex_apps_mcp_server_config(
|
|
&mcp_config.chatgpt_base_url,
|
|
mcp_config.apps_mcp_product_sku.as_deref(),
|
|
originator,
|
|
),
|
|
));
|
|
} else {
|
|
catalog.remove_compatibility(
|
|
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
LEGACY_CODEX_APPS_REGISTRATION_ID,
|
|
);
|
|
}
|
|
|
|
for overlay in overlays {
|
|
match overlay {
|
|
OrderedMcpOverlay::Set(registration) => catalog.register(*registration),
|
|
OrderedMcpOverlay::Remove {
|
|
contributor_id,
|
|
contribution_order,
|
|
name,
|
|
} => catalog.remove_extension(name, contributor_id, contribution_order),
|
|
}
|
|
}
|
|
let selections = match environment_scope {
|
|
McpEnvironmentScope::HostOnly => None,
|
|
McpEnvironmentScope::Initial(selections) => Some(selections.to_vec()),
|
|
McpEnvironmentScope::Live(environments) => Some(environments.selections()),
|
|
};
|
|
let catalog = catalog.build_with_environment_authority(|environment_id| {
|
|
let Some(selections) = selections.as_ref() else {
|
|
return McpEnvironmentAuthority::Unrestricted;
|
|
};
|
|
let Some(selection) = selections
|
|
.iter()
|
|
.find(|selection| selection.environment_id == environment_id)
|
|
else {
|
|
return if environment_id == DEFAULT_MCP_SERVER_ENVIRONMENT_ID {
|
|
McpEnvironmentAuthority::Unrestricted
|
|
} else {
|
|
McpEnvironmentAuthority::SelectedPluginsOnly
|
|
};
|
|
};
|
|
|
|
match &selection.config {
|
|
EnvironmentConfigState::FromThread => McpEnvironmentAuthority::Unrestricted,
|
|
EnvironmentConfigState::Pending | EnvironmentConfigState::Failed(_) => {
|
|
McpEnvironmentAuthority::Unavailable
|
|
}
|
|
EnvironmentConfigState::Ready(config) => config
|
|
.mcp_policy
|
|
.as_ref()
|
|
.map_or(McpEnvironmentAuthority::Unrestricted, |policy| {
|
|
McpEnvironmentAuthority::Restricted(policy)
|
|
}),
|
|
}
|
|
});
|
|
for conflict in catalog.conflicts() {
|
|
tracing::warn!(
|
|
server = conflict.name,
|
|
outcome = ?conflict.outcome,
|
|
contenders = ?conflict.contenders,
|
|
"conflicting MCP server actions; using resolved catalog outcome"
|
|
);
|
|
}
|
|
mcp_config.mcp_server_catalog = catalog;
|
|
mcp_config.connector_snapshot =
|
|
mcp_config
|
|
.connector_snapshot
|
|
.merged_with(&ConnectorSnapshot::from_plugin_sources(
|
|
selected_plugin_connector_sources,
|
|
));
|
|
McpRuntimeProjection {
|
|
config: mcp_config,
|
|
plugins_available,
|
|
selected_plugins: SelectedPluginSnapshot {
|
|
plugins: selected_plugins,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Returns config- and plugin-backed servers without runtime contributions.
|
|
pub async fn configured_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
|
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
|
configured_mcp_servers(&mcp_config)
|
|
}
|
|
|
|
/// Returns configured and host-contributed servers before auth gating.
|
|
pub async fn runtime_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
|
let mcp_config = self.runtime_config(config).await;
|
|
configured_mcp_servers(&mcp_config)
|
|
}
|
|
|
|
/// Returns runtime servers after auth gating and compatibility built-ins.
|
|
pub async fn effective_servers(
|
|
&self,
|
|
config: &Config,
|
|
auth: Option<&CodexAuth>,
|
|
) -> HashMap<String, EffectiveMcpServer> {
|
|
let mcp_config = self.runtime_config(config).await;
|
|
effective_mcp_servers(&mcp_config, auth)
|
|
}
|
|
}
|