mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Accept explicit contribution context for MCP runtime configuration (#61283)
## What changed - Make `McpManager::runtime_config` accept `McpServerContributionContext` and honor its originator, selected environments, and disabled plugin IDs. - Add `McpServerContributionContext::for_host` to carry thread initialization data, originator, and session source without live extension state or discovery. - Pass explicit global contexts from threadless discovery and control paths. ## Testing Extend the thread manager test to cover host-only contribution identity, originator headers, and server restrictions for disabled plugins and pending environments. GitOrigin-RevId: 04ea2c8951187d9014867f0020223b6a7dd94d8d
This commit is contained in:
@@ -4,6 +4,7 @@ use codex_connectors::ConnectorRuntimeTool;
|
||||
use codex_connectors::connector_runtime_context_key;
|
||||
use codex_connectors::connector_tool_is_synthetic;
|
||||
use codex_connectors::installed_connector_runtime;
|
||||
use codex_extension_api::McpServerContributionContext;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY;
|
||||
use codex_mcp::McpRuntime;
|
||||
@@ -79,7 +80,7 @@ impl AppsRequestProcessor {
|
||||
snapshot_age = Some(Duration::ZERO);
|
||||
return Ok(snapshot.tools);
|
||||
}
|
||||
let mcp_config = mcp_manager.runtime_config(&config).await;
|
||||
let mcp_config = mcp_manager.runtime_config(McpServerContributionContext::global(&config)).await;
|
||||
let mut mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
|
||||
let codex_apps_server = codex_mcp::McpServerKey::unscoped(CODEX_APPS_MCP_SERVER_NAME);
|
||||
mcp_servers.retain(|key, _| key == &codex_apps_server);
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::thread_input::ensure_direct_input_allowed;
|
||||
use super::*;
|
||||
use codex_config::types::McpServerAuth;
|
||||
use codex_core::McpRefreshReason;
|
||||
use codex_extension_api::McpServerContributionContext;
|
||||
use codex_mcp::McpServerSource;
|
||||
use codex_mcp::ReadResourceRequestParams;
|
||||
use codex_mcp::ema_auth_scope;
|
||||
@@ -156,7 +157,7 @@ impl McpRequestProcessor {
|
||||
let mcp_config = self
|
||||
.thread_manager
|
||||
.mcp_manager()
|
||||
.runtime_config(&config)
|
||||
.runtime_config(McpServerContributionContext::global(&config))
|
||||
.await;
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
self.thread_manager.environment_manager(),
|
||||
@@ -458,7 +459,9 @@ impl McpRequestProcessor {
|
||||
let (mcp_config, runtime_context) = match thread.as_ref() {
|
||||
Some(thread) => thread.runtime_mcp_config_and_context(&config).await,
|
||||
None => (
|
||||
mcp_manager.runtime_config(&config).await,
|
||||
mcp_manager
|
||||
.runtime_config(McpServerContributionContext::global(&config))
|
||||
.await,
|
||||
McpRuntimeContext::new(
|
||||
self.thread_manager.environment_manager(),
|
||||
config.cwd.to_path_buf(),
|
||||
@@ -744,7 +747,9 @@ impl McpRequestProcessor {
|
||||
|
||||
let config = self.load_global_config().await?;
|
||||
let mcp_manager = self.thread_manager.mcp_manager();
|
||||
let mcp_config = mcp_manager.runtime_config(&config).await;
|
||||
let mcp_config = mcp_manager
|
||||
.runtime_config(McpServerContributionContext::global(&config))
|
||||
.await;
|
||||
let codex_apps_tools_cache = mcp_manager.codex_apps_tools_cache();
|
||||
let tool_catalog_cache = mcp_manager.tool_catalog_cache();
|
||||
self.thread_manager
|
||||
|
||||
@@ -23,6 +23,7 @@ use codex_core::plugins_manager_for_config;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::HttpClient;
|
||||
use codex_exec_server::RouteAwareHttpClient;
|
||||
use codex_extension_api::McpServerContributionContext;
|
||||
use codex_login::AuthManager;
|
||||
use codex_mcp::McpOAuthLoginSupport;
|
||||
use codex_mcp::McpRuntimeContext;
|
||||
@@ -692,7 +693,9 @@ async fn run_list(config: &Config, list_args: ListArgs) -> Result<()> {
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await?;
|
||||
let auth = auth_manager.auth().await;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
let runtime_config = mcp_manager.runtime_config(config).await;
|
||||
let runtime_config = mcp_manager
|
||||
.runtime_config(McpServerContributionContext::global(config))
|
||||
.await;
|
||||
let effective_mcp_servers = codex_mcp::effective_mcp_servers(&runtime_config, auth.as_ref());
|
||||
|
||||
let mut entries: Vec<_> = mcp_servers.iter().collect();
|
||||
|
||||
@@ -27,6 +27,7 @@ use crate::session::INITIAL_SUBMIT_ID;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_extension_api::McpServerContributionContext;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -213,7 +214,9 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
});
|
||||
}
|
||||
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
|
||||
let mcp_config = mcp_manager.runtime_config(config).await;
|
||||
let mcp_config = mcp_manager
|
||||
.runtime_config(McpServerContributionContext::global(config))
|
||||
.await;
|
||||
let tool_plugin_context = tool_plugin_context(&mcp_config);
|
||||
if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key)
|
||||
{
|
||||
|
||||
+17
-11
@@ -199,16 +199,18 @@ impl McpManager {
|
||||
}
|
||||
|
||||
/// Returns the MCP config after applying compatibility built-ins and
|
||||
/// runtime-only extension overlays.
|
||||
pub async fn runtime_config(&self, config: &Config) -> McpConfig {
|
||||
/// runtime-only extension overlays and restrictions from the supplied contribution context.
|
||||
pub async fn runtime_config(
|
||||
&self,
|
||||
context: McpServerContributionContext<'_, 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,
|
||||
&[],
|
||||
context,
|
||||
context.originator(),
|
||||
context
|
||||
.selected_environments()
|
||||
.map_or(McpEnvironmentScope::HostOnly, McpEnvironmentScope::Selected),
|
||||
context.disabled_plugin_ids(),
|
||||
)
|
||||
.await
|
||||
.config
|
||||
@@ -565,7 +567,9 @@ impl McpManager {
|
||||
|
||||
/// 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;
|
||||
let mcp_config = self
|
||||
.runtime_config(McpServerContributionContext::global(config))
|
||||
.await;
|
||||
configured_mcp_servers(&mcp_config)
|
||||
}
|
||||
|
||||
@@ -575,7 +579,9 @@ impl McpManager {
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> HashMap<codex_mcp::McpServerKey, EffectiveMcpServer> {
|
||||
let mcp_config = self.runtime_config(config).await;
|
||||
let mcp_config = self
|
||||
.runtime_config(McpServerContributionContext::global(config))
|
||||
.await;
|
||||
effective_mcp_servers(&mcp_config, auth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2131,7 +2131,7 @@ async fn spawn_internal_session_preserves_parent_lineage_without_forking_history
|
||||
async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() {
|
||||
struct InitialDataRecorder {
|
||||
lifecycle_observed: Arc<std::sync::Mutex<Vec<(String, String)>>>,
|
||||
mcp_observed: Arc<std::sync::Mutex<Vec<(String, SessionSource)>>>,
|
||||
mcp_observed: Arc<std::sync::Mutex<Vec<(String, SessionSource, bool)>>>,
|
||||
mcp_loaded: Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
|
||||
@@ -2177,11 +2177,13 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
) -> codex_extension_api::ExtensionFuture<'a, Vec<codex_extension_api::SelectedPlugin<'a>>>
|
||||
{
|
||||
Box::pin(async move {
|
||||
assert_eq!(
|
||||
context.selected_environments(),
|
||||
Some([].as_slice()),
|
||||
"thread MCP projection must preserve explicitly empty selections"
|
||||
);
|
||||
if context.thread_store().is_some() {
|
||||
assert_eq!(
|
||||
context.selected_environments(),
|
||||
Some([].as_slice()),
|
||||
"thread MCP projection must preserve explicitly empty selections"
|
||||
);
|
||||
}
|
||||
let thread_init = context
|
||||
.thread_init()
|
||||
.expect("initial MCP resolution should be thread-scoped");
|
||||
@@ -2198,6 +2200,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
.session_source()
|
||||
.expect("thread-scoped MCP resolution should identify its source")
|
||||
.clone(),
|
||||
context.thread_store().is_some(),
|
||||
));
|
||||
let mut server = codex_mcp::codex_apps_mcp_server_config(
|
||||
"https://selected.invalid",
|
||||
@@ -2208,7 +2211,8 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
&selected_root.location;
|
||||
let source_environment_id = environment_id.clone();
|
||||
server.environment_id = source_environment_id.clone();
|
||||
server.enabled = false;
|
||||
// Keep thread startup offline; host-only checks below never open connections.
|
||||
server.enabled = context.thread_store().is_none();
|
||||
let selected_root_id = selected_root.id;
|
||||
let plugin_id = format!("plugin-{selected_root_id}");
|
||||
let mcp_loaded = Arc::clone(&self.mcp_loaded);
|
||||
@@ -2315,26 +2319,16 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
.expect("start second thread");
|
||||
let first_session = &first_thread.thread.session;
|
||||
let first_originator = first_session.originator().await;
|
||||
let host_context = codex_extension_api::McpServerContributionContext::for_host(
|
||||
&config,
|
||||
Some(&first_session.services.mcp_thread_init),
|
||||
Some(&first_originator),
|
||||
Some(&SessionSource::Exec),
|
||||
);
|
||||
let first_resolved = first_session
|
||||
.services
|
||||
.mcp_manager
|
||||
.runtime_config_for_step(
|
||||
&config,
|
||||
&first_session.services.mcp_thread_init,
|
||||
&first_session.services.thread_extension_data,
|
||||
McpThreadIdentity {
|
||||
auth_changed: false,
|
||||
session_source: &SessionSource::Exec,
|
||||
originator: &first_originator,
|
||||
disabled_plugin_ids: &[],
|
||||
environments: McpEnvironmentScope::Selected(
|
||||
&first_session.services.turn_environments.selections(),
|
||||
),
|
||||
},
|
||||
/*ready_selected_capability_roots*/ &[],
|
||||
/*executor_capability_discovery*/ None,
|
||||
/*executor_capability_discovery_v2*/ None,
|
||||
)
|
||||
.runtime_config(host_context)
|
||||
.await;
|
||||
let second_session = &second_thread.thread.session;
|
||||
let second_originator = second_session.originator().await;
|
||||
@@ -2377,10 +2371,14 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner),
|
||||
vec![
|
||||
("selected-a".to_string(), SessionSource::Exec),
|
||||
("selected-b".to_string(), second_session_source.clone()),
|
||||
("selected-a".to_string(), SessionSource::Exec),
|
||||
("selected-b".to_string(), second_session_source),
|
||||
("selected-a".to_string(), SessionSource::Exec, true),
|
||||
(
|
||||
"selected-b".to_string(),
|
||||
second_session_source.clone(),
|
||||
true
|
||||
),
|
||||
("selected-a".to_string(), SessionSource::Exec, false),
|
||||
("selected-b".to_string(), second_session_source, true),
|
||||
]
|
||||
);
|
||||
let selected_servers = |config: &codex_mcp::McpConfig| {
|
||||
@@ -2391,7 +2389,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
.collect::<std::collections::BTreeMap<_, _>>()
|
||||
};
|
||||
assert_eq!(
|
||||
selected_servers(&first_resolved.config),
|
||||
selected_servers(&first_resolved),
|
||||
std::collections::BTreeMap::from([("selected-a".to_string(), "env-a".to_string())])
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2399,7 +2397,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
std::collections::BTreeMap::from([("selected-b".to_string(), "env-b".to_string())])
|
||||
);
|
||||
for (config, name, source_environment_id) in [
|
||||
(&first_resolved.config, "selected-a", "env-a"),
|
||||
(&first_resolved, "selected-a", "env-a"),
|
||||
(&second_resolved.config, "selected-b", "env-b"),
|
||||
] {
|
||||
let key = codex_mcp::McpServerKey::in_environment(source_environment_id, name);
|
||||
@@ -2418,7 +2416,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
));
|
||||
assert_eq!(Some(server), expected.build().server(&key));
|
||||
}
|
||||
let codex_apps_server = codex_mcp::configured_mcp_servers(&first_resolved.config)
|
||||
let codex_apps_server = codex_mcp::configured_mcp_servers(&first_resolved)
|
||||
.remove(codex_mcp::CODEX_APPS_MCP_SERVER_NAME)
|
||||
.expect("Codex Apps server should be configured");
|
||||
let codex_apps_headers = match codex_apps_server.transport {
|
||||
@@ -2433,6 +2431,38 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
.get("originator"),
|
||||
Some(&"codex_work_desktop".to_string())
|
||||
);
|
||||
// A supplied plugin denial or unavailable environment must also restrict host projections.
|
||||
let disabled_plugins = ["plugin-selected-a".to_string()];
|
||||
let pending_environments = [codex_protocol::protocol::TurnEnvironmentSelection {
|
||||
environment_id: "env-a".to_string(),
|
||||
cwd: PathUri::parse("file:///plugins").expect("environment cwd"),
|
||||
workspace_roots: vec![],
|
||||
config: EnvironmentConfigState::Pending,
|
||||
selected_capability_roots: Default::default(),
|
||||
}];
|
||||
for (context, enabled) in [
|
||||
(host_context, true),
|
||||
(
|
||||
host_context.with_disabled_plugin_ids(&disabled_plugins),
|
||||
false,
|
||||
),
|
||||
(
|
||||
host_context.with_selected_environments(&pending_environments),
|
||||
false,
|
||||
),
|
||||
] {
|
||||
let resolved = first_session
|
||||
.services
|
||||
.mcp_manager
|
||||
.runtime_config(context)
|
||||
.await;
|
||||
assert_eq!(
|
||||
codex_mcp::configured_mcp_servers(&resolved)
|
||||
.get("selected-a")
|
||||
.is_some_and(|server| server.enabled),
|
||||
enabled,
|
||||
);
|
||||
}
|
||||
for (policy, disabled_plugin_ids, selected_enabled, expected_mcp_loads) in [
|
||||
("", vec!["plugin-selected-a".to_string()], false, 1),
|
||||
("", vec![], true, 1),
|
||||
|
||||
@@ -66,6 +66,21 @@ impl<'a, C> McpServerContributionContext<'a, C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates host-only context with thread identity but no live extension state or discovery.
|
||||
pub fn for_host(
|
||||
config: &'a C,
|
||||
thread_init: Option<&'a ExtensionDataInit>,
|
||||
originator: Option<&'a str>,
|
||||
session_source: Option<&'a SessionSource>,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread_init,
|
||||
originator,
|
||||
session_source,
|
||||
..Self::global(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates context for one model step using only currently available environments.
|
||||
pub fn for_step(
|
||||
config: &'a C,
|
||||
|
||||
Reference in New Issue
Block a user