Make plugin guidance react to environment readiness (#30223)

## Why

Generic plugin guidance is currently emitted only with initial context
from host plugin state. An executor-selected plugin can become available
later in the same turn, making its skills and tools usable without ever
telling the model how plugin capabilities should be used.

## What

- project every ready selected plugin package, including skill-only
plugins
- carry plugin availability with the exact MCP runtime projection while
preserving MCP manager reuse when servers and connectors are unchanged
- move generic plugin guidance from the static initial-context path into
persisted World State
- recognize legacy and retained plugin fragments so resume and
compaction do not duplicate guidance

## Testing

- `just test -p codex-mcp-extension`
- `just test -p codex-core plugins_instructions`
- `just test -p codex-core
plugin_availability_change_reuses_the_mcp_manager`
- `just test -p codex-app-server --test all selected_capabilit`
This commit is contained in:
sayan-oai
2026-07-06 07:05:40 -07:00
committed by GitHub
parent be33f80bc6
commit 9c5be7e1d5
21 changed files with 443 additions and 94 deletions
+18 -8
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::mcp::McpRuntimeProjection;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
use codex_mcp::ElicitationReviewRequest;
use codex_mcp::ElicitationReviewer;
@@ -92,6 +93,7 @@ impl Session {
&available_environment_ids,
)
.await
.config
}
pub(crate) async fn runtime_mcp_servers(
@@ -123,7 +125,7 @@ impl Session {
if current.available_environment_ids() == available_environment_ids {
return current;
}
let mcp_config = self
let mcp_projection = self
.services
.mcp_manager
.runtime_config_for_step(
@@ -133,6 +135,7 @@ impl Session {
&available_environment_ids,
)
.await;
let mcp_config = &mcp_projection.config;
let changed_environment_is_used_by_mcp = mcp_config
.mcp_server_catalog
.configured_servers()
@@ -156,6 +159,7 @@ impl Session {
// replacing the live manager and restarting its processes.
let runtime = Arc::new(McpRuntimeSnapshot::new(
Arc::new(current.config().clone()),
mcp_projection.plugins_available,
current.manager_arc(),
current.runtime_context().clone(),
available_environment_ids,
@@ -165,7 +169,7 @@ impl Session {
}
self.refresh_mcp_servers_inner(
turn_context,
mcp_config,
mcp_projection,
environments,
&available_environment_ids,
Some(self.mcp_elicitation_reviewer()),
@@ -309,12 +313,16 @@ impl Session {
async fn refresh_mcp_servers_inner(
&self,
turn_context: &TurnContext,
mcp_config: McpConfig,
mcp_projection: McpRuntimeProjection,
environments: &TurnEnvironmentSnapshot,
available_environment_ids: &[String],
elicitation_reviewer: Option<ElicitationReviewerHandle>,
) -> Arc<McpRuntimeSnapshot> {
let auth = self.services.auth_manager.auth().await;
let McpRuntimeProjection {
config: mcp_config,
plugins_available,
} = mcp_projection;
let mcp_config = Arc::new(mcp_config);
let tool_plugin_provenance = codex_mcp::tool_plugin_provenance(&mcp_config);
let mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
@@ -376,6 +384,7 @@ impl Session {
.set_elicitations_auto_deny(current_runtime.manager().elicitations_auto_deny());
self.services.publish_mcp_runtime(
mcp_config,
plugins_available,
mcp_runtime_context,
available_environment_ids.to_vec(),
refreshed_manager,
@@ -448,7 +457,7 @@ impl Session {
.latest_mcp_runtime()
.available_environment_ids()
.to_vec();
let mut mcp_config = self
let mut mcp_projection = self
.services
.mcp_manager
.runtime_config_for_step(
@@ -458,12 +467,13 @@ impl Session {
&available_environment_ids,
)
.await;
mcp_config.mcp_server_catalog = mcp_config
mcp_projection.config.mcp_server_catalog = mcp_projection
.config
.mcp_server_catalog
.with_materialized_servers(mcp_servers);
self.refresh_mcp_servers_inner(
turn_context,
mcp_config,
mcp_projection,
&turn_context.environments,
&available_environment_ids,
elicitation_reviewer,
@@ -515,7 +525,7 @@ impl Session {
.latest_mcp_runtime()
.available_environment_ids()
.to_vec();
let mcp_config = self
let mcp_projection = self
.services
.mcp_manager
.runtime_config_for_step(
@@ -527,7 +537,7 @@ impl Session {
.await;
self.refresh_mcp_servers_inner(
turn_context,
mcp_config,
mcp_projection,
&turn_context.environments,
&available_environment_ids,
elicitation_reviewer,
+9 -1
View File
@@ -5,9 +5,10 @@ use codex_mcp::McpConfig;
use codex_mcp::McpConnectionManager;
use codex_mcp::McpRuntimeContext;
/// MCP config, exact environment bindings, and manager used by one model request.
/// MCP config, plugin availability, exact environment bindings, and manager for one request.
pub struct McpRuntimeSnapshot {
config: Arc<McpConfig>,
plugins_available: bool,
manager: Arc<McpConnectionManager>,
runtime_context: McpRuntimeContext,
available_environment_ids: Vec<String>,
@@ -16,12 +17,14 @@ pub struct McpRuntimeSnapshot {
impl McpRuntimeSnapshot {
pub(crate) fn new(
config: Arc<McpConfig>,
plugins_available: bool,
manager: Arc<McpConnectionManager>,
runtime_context: McpRuntimeContext,
available_environment_ids: Vec<String>,
) -> Self {
Self {
config,
plugins_available,
manager,
runtime_context,
available_environment_ids,
@@ -32,6 +35,10 @@ impl McpRuntimeSnapshot {
self.config.as_ref()
}
pub(crate) fn plugins_available(&self) -> bool {
self.plugins_available
}
pub fn manager(&self) -> &McpConnectionManager {
self.manager.as_ref()
}
@@ -86,6 +93,7 @@ impl McpRuntimeSnapshot {
);
Arc::new(Self::new(
Arc::new(mcp_config),
/*plugins_available*/ false,
Arc::new(manager),
runtime_context,
Vec::new(),
-6
View File
@@ -24,7 +24,6 @@ use crate::config::resolve_tool_suggest_config_from_layer_stack;
use crate::connectors;
use crate::context::ApprovedCommandPrefixSaved;
use crate::context::AppsInstructions;
use crate::context::AvailablePluginsInstructions;
use crate::context::AvailableSkillsInstructions;
use crate::context::CollaborationModeInstructions;
use crate::context::ContextualUserFragment;
@@ -3306,11 +3305,6 @@ impl Session {
{
contextual_user_sections.push(recommended_plugins.render());
}
if let Some(plugin_instructions) =
AvailablePluginsInstructions::from_plugins(loaded_plugins.capability_summaries())
{
developer_sections.push(plugin_instructions.render());
}
let context_contributors = self.services.extensions.context_contributors().to_vec();
for contributor in &context_contributors {
for fragment in contributor
+12 -7
View File
@@ -679,7 +679,7 @@ impl Session {
let mcp_runtime_context_for_auth = mcp_runtime_context.clone();
let auth_and_mcp_fut = async move {
let auth = auth_manager_clone.auth().await;
let mcp_config = mcp_manager_for_mcp
let mcp_projection = mcp_manager_for_mcp
.runtime_config_for_step(
&config_for_mcp,
mcp_thread_init_for_startup,
@@ -687,8 +687,9 @@ impl Session {
/*available_environment_ids*/ &[],
)
.await;
let mcp_servers = codex_mcp::effective_mcp_servers(&mcp_config, auth.as_ref());
let tool_plugin_provenance = codex_mcp::tool_plugin_provenance(&mcp_config);
let mcp_config = &mcp_projection.config;
let mcp_servers = codex_mcp::effective_mcp_servers(mcp_config, auth.as_ref());
let tool_plugin_provenance = codex_mcp::tool_plugin_provenance(mcp_config);
let auth_statuses = compute_auth_statuses(
mcp_servers.iter(),
config_for_mcp.mcp_oauth_credentials_store_mode,
@@ -699,7 +700,7 @@ impl Session {
.await;
(
auth,
mcp_config,
mcp_projection,
mcp_servers,
auth_statuses,
tool_plugin_provenance,
@@ -714,7 +715,7 @@ impl Session {
let (
thread_persistence_result,
state_db_ctx,
(auth, mcp_config, mcp_servers, auth_statuses, tool_plugin_provenance),
(auth, mcp_projection, mcp_servers, auth_statuses, tool_plugin_provenance),
) = tokio::join!(thread_persistence_fut, state_db_fut, auth_and_mcp_fut);
let mut live_thread_init =
@@ -1208,7 +1209,10 @@ impl Session {
sess.services.mcp_manager.codex_apps_tools_cache(),
codex_apps_tools_cache_key(auth),
config.prefix_mcp_tool_names(),
mcp_config.client_elicitation_capability.clone(),
mcp_projection
.config
.client_elicitation_capability
.clone(),
sess.services
.supports_openai_form_elicitation
.load(std::sync::atomic::Ordering::Relaxed),
@@ -1224,7 +1228,8 @@ impl Session {
.await;
sess.services
.install_mcp_connection_manager(
Arc::new(mcp_config),
Arc::new(mcp_projection.config),
mcp_projection.plugins_available,
mcp_runtime_context,
/*available_environment_ids*/ Vec::new(),
mcp_connection_manager,
+85
View File
@@ -7729,6 +7729,91 @@ async fn refresh_mcp_servers_keeps_the_previous_runtime_alive() {
);
}
#[tokio::test]
async fn plugin_availability_change_reuses_the_mcp_manager() {
struct ReadyPluginContributor;
impl codex_extension_api::McpServerContributor<Config> for ReadyPluginContributor {
fn id(&self) -> &'static str {
"ready_plugin_test"
}
fn contribute<'a>(
&'a self,
context: codex_extension_api::McpServerContributionContext<'a, Config>,
) -> codex_extension_api::ExtensionFuture<'a, Vec<codex_extension_api::McpServerContribution>>
{
Box::pin(async move {
let available = context
.available_environment_ids()
.is_some_and(|ids| ids.iter().any(|id| id == "executor"));
available
.then(
|| codex_extension_api::McpServerContribution::SelectedPluginPackage {
plugin_id: "skill-only".to_string(),
plugin_display_name: "Skill Only".to_string(),
connector_ids: Vec::new(),
},
)
.into_iter()
.collect()
})
}
}
let (mut session, turn_context) = make_session_and_context().await;
let mut registry = codex_extension_api::ExtensionRegistryBuilder::new();
registry.mcp_server_contributor(Arc::new(ReadyPluginContributor));
let registry = Arc::new(registry.build());
session.services.extensions = Arc::clone(&registry);
session.services.mcp_manager = Arc::new(McpManager::new_with_extensions(
Arc::clone(&session.services.plugins_manager),
registry,
));
let session = Arc::new(session);
session
.refresh_mcp_servers_now(
&turn_context,
&turn_context.config,
/*elicitation_reviewer*/ None,
)
.await;
let old_runtime = session.services.latest_mcp_runtime();
let old_manager = old_runtime.manager_arc();
let selected_root = codex_protocol::capabilities::SelectedCapabilityRoot {
id: "skill-only".to_string(),
location: codex_protocol::capabilities::CapabilityRootLocation::Environment {
environment_id: "executor".to_string(),
path: PathUri::from_host_native_path(turn_context.config.cwd.as_path())
.expect("selected capability root URI"),
},
};
let environment = Arc::clone(
&turn_context
.environments
.primary()
.expect("ready test environment")
.environment,
);
let captured_environments = HashMap::from([("executor".to_string(), Some(environment))]);
let resolved_roots = session
.services
.turn_environments
.environment_manager()
.resolve_selected_capability_roots(&[selected_root], &captured_environments)
.await;
let new_runtime = session
.mcp_runtime_for_step(&turn_context, &turn_context.environments, &resolved_roots)
.await;
assert!(!old_runtime.plugins_available());
assert!(new_runtime.plugins_available());
assert!(!Arc::ptr_eq(&old_runtime, &new_runtime));
assert!(Arc::ptr_eq(&old_manager, &new_runtime.manager_arc()));
}
#[tokio::test]
async fn built_tools_uses_the_step_mcp_runtime() -> anyhow::Result<()> {
let (session, turn_context) = make_session_and_context().await;
+4
View File
@@ -2,6 +2,7 @@ use super::session::Session;
use super::step_context::StepContext;
use crate::context::world_state::AgentsMdState;
use crate::context::world_state::EnvironmentsState;
use crate::context::world_state::PluginsInstructionsState;
use crate::context::world_state::WorldState;
use codex_extension_api::WorldStateContributionInput;
@@ -35,6 +36,9 @@ impl Session {
.with_subagents(environment_subagents),
);
}
world_state.add_section(PluginsInstructionsState::new(
step_context.mcp.plugins_available(),
));
let environments = step_context.environments.to_selections();
let ready_selected_capability_roots = step_context
.selected_capability_roots