mirror of
https://github.com/openai/codex.git
synced 2026-10-29 00:06:55 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(®istry);
|
||||
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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user