Unify plugin skill loading with the host skill service (#37444)

## What changed

- Inject the host skill loader into `PluginsManager` so plugin discovery and agent turns use the same loading and product-policy behavior.
- Share plugin skill snapshots across those paths, preserving a consistent view of skills across workspaces.
- Apply migrated-command precedence after product filtering, allowing an eligible migrated command to replace a filtered native skill with the same name.

## Testing

- Add coverage for product-restricted plugin skills, native-versus-migrated command precedence, and the skills exposed to agent turns.

GitOrigin-RevId: f5ef0d0766ebeeb30d73ffaf044d003c2906ea4d
This commit is contained in:
felixxia-oai
2026-08-07 13:54:24 +00:00
committed by copyberry
parent e75a1888d7
commit e58d9ef447
30 changed files with 682 additions and 272 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
use super::*;
use crate::HostSkillsService;
use crate::config::ConfigBuilder;
use crate::plugins::plugins_manager_for_config;
use crate::skills_load_input_from_config;
use codex_core_plugins::PluginsManager;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ReasoningEffort;
use codex_utils_absolute_path::test_support::PathExt;
@@ -398,7 +398,7 @@ enabled = false
.await
.expect("custom role should apply");
let plugins_manager = Arc::new(PluginsManager::new(home.path().to_path_buf()));
let plugins_manager = Arc::new(plugins_manager_for_config(&config));
let skills_service =
HostSkillsService::new(home.path().abs(), /*bundled_skills_enabled*/ true);
let plugins_input = config.plugins_config_input();
+10 -10
View File
@@ -1,6 +1,7 @@
use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder;
use crate::config::edit::apply_blocking;
use crate::plugins::plugins_manager_for_config;
use assert_matches::assert_matches;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerEntry;
@@ -67,7 +68,6 @@ use codex_config::types::TuiNotificationSettings;
use codex_config::types::TuiPetAnchor;
use codex_config::types::WindowsSandboxModeToml;
use codex_config::types::WindowsToml;
use codex_core_plugins::PluginsManager;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
use codex_features::FeaturesToml;
@@ -499,7 +499,7 @@ async fn load_config_resolves_non_prefixed_mcp_tool_servers() -> std::io::Result
assert_eq!(config.non_prefixed_mcp_tool_servers, expected_servers);
assert_eq!(config.prefix_mcp_tool_names(), expected_prefix);
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert_eq!(mcp_config.prefix_mcp_tool_names, expected_prefix);
assert_eq!(
@@ -5214,7 +5214,7 @@ async fn rebuild_preserving_session_layers_refreshes_plugin_derived_mcp_config()
let config = thread_config
.rebuild_preserving_session_layers(&refreshed_config)
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
let configured_servers = mcp_config.mcp_server_catalog.configured_servers();
@@ -5276,7 +5276,7 @@ enabled = true
.codex_home(codex_home.path().to_path_buf())
.build()
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
let configured_servers = mcp_config.mcp_server_catalog.configured_servers();
@@ -5344,7 +5344,7 @@ url = "https://sample.example/mcp"
)
.build()
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
let configured_servers = mcp_config.mcp_server_catalog.configured_servers();
@@ -5446,7 +5446,7 @@ enabled = true
)
.build()
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
let configured_servers = mcp_config.mcp_server_catalog.configured_servers();
@@ -6463,7 +6463,7 @@ async fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<(
codex_home.abs(),
)
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
config.apps_mcp_product_sku = Some("tpp".to_string());
let mcp_config = config.to_mcp_config(&plugins_manager).await;
@@ -6490,7 +6490,7 @@ async fn to_mcp_config_flows_mcp_tool_prefix_from_feature() -> std::io::Result<(
codex_home.abs(),
)
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert!(mcp_config.prefix_mcp_tool_names);
@@ -6526,7 +6526,7 @@ async fn to_mcp_config_flows_mcp_2026_feature_from_config() -> std::io::Result<(
codex_home.abs(),
)
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert_eq!(mcp_config.protocol_mode, codex_mcp::McpProtocolMode::Legacy);
@@ -6550,7 +6550,7 @@ async fn to_mcp_config_preserves_auth_elicitation_feature_from_config() -> std::
codex_home.abs(),
)
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert_eq!(
+2 -1
View File
@@ -22,6 +22,7 @@ use tracing::warn;
use crate::config::Config;
use crate::mcp::McpManager;
use crate::plugins::list_tool_suggest_discoverable_plugins;
use crate::plugins::plugins_manager_for_config;
use crate::session::INITIAL_SUBMIT_ID;
use codex_config::types::ApprovalsReviewer;
use codex_config::types::ToolSuggestDiscoverableType;
@@ -186,7 +187,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
force_refetch: bool,
environment_manager: Arc<EnvironmentManager>,
) -> anyhow::Result<AccessibleConnectorsStatus> {
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
let plugins_manager = Arc::new(plugins_manager_for_config(config));
let mcp_manager = Arc::new(McpManager::new(plugins_manager));
list_accessible_connectors_from_mcp_tools_with_mcp_manager(
config,
+3 -2
View File
@@ -1,6 +1,7 @@
use super::*;
use crate::config::CONFIG_TOML_FILE;
use crate::config::ConfigBuilder;
use crate::plugins::plugins_manager_for_config;
use codex_config::test_support::CloudConfigBundleFixture;
use codex_config::types::ApprovalsReviewer;
use codex_connectors::merge::plugin_connector_to_app_info;
@@ -504,7 +505,7 @@ discoverables = [
.await
.expect("config should load");
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(
&config,
@@ -542,7 +543,7 @@ apps = true
.expect("config should load");
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let loaded_plugin_app_connector_ids = vec!["asdk_app_databricks_workspace".to_string()];
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(
&config,
+1
View File
@@ -74,6 +74,7 @@ pub use mention_syntax::PLUGIN_TEXT_MENTION_SIGIL;
pub use mention_syntax::TOOL_MENTION_SIGIL;
pub use utils::path_utils;
pub(crate) mod plugins;
pub use plugins::plugins_manager_for_config;
#[doc(hidden)]
pub(crate) mod prompt_debug;
#[doc(hidden)]
@@ -1,7 +1,7 @@
use crate::plugins::plugins_manager_for_config;
use crate::plugins::test_support::load_plugins_config;
use crate::plugins::test_support::write_file;
use crate::plugins::test_support::write_openai_curated_marketplace;
use codex_core_plugins::PluginsManager;
use codex_core_plugins::startup_sync::curated_plugins_repo_path;
use codex_tools::DiscoverablePluginInfo;
use pretty_assertions::assert_eq;
@@ -11,7 +11,7 @@ async fn list_discoverable_plugins(
config: &crate::config::Config,
loaded_plugin_app_connector_ids: &[String],
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
let plugins_manager = plugins_manager_for_config(config);
super::list_tool_suggest_discoverable_plugins(
config,
&plugins_manager,
+16
View File
@@ -8,6 +8,11 @@ mod skill_snapshot_tests;
#[cfg(test)]
pub(crate) mod test_support;
use crate::config::Config;
use codex_core_plugins::PluginsManager;
use codex_skills_extension::HostSkillsService;
use std::sync::Arc;
pub(crate) use codex_plugin::PluginCapabilitySummary;
pub(crate) use discoverable::list_tool_suggest_discoverable_plugins;
@@ -19,3 +24,14 @@ pub(crate) use mentions::build_skill_name_counts;
pub(crate) use mentions::collect_explicit_app_ids;
pub(crate) use mentions::collect_explicit_plugin_mentions;
pub(crate) use mentions::collect_tool_mentions_from_messages;
/// Constructs a standalone plugin manager with extension-owned plugin skill loading.
///
/// Callers that already own a host skills service should inject that existing service instead.
pub fn plugins_manager_for_config(config: &Config) -> PluginsManager {
let skill_root_loader = Arc::new(HostSkillsService::new(
config.codex_home.clone(),
/*bundled_skills_enabled*/ false,
));
PluginsManager::new(config.codex_home.to_path_buf(), skill_root_loader)
}
@@ -5,7 +5,9 @@ use codex_protocol::auth::AuthMode;
use codex_protocol::protocol::Product;
use codex_skills_extension::HostSkillsLoadInput;
use codex_skills_extension::HostSkillsService;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::sync::Arc;
use super::test_support::load_plugins_config;
use super::test_support::write_file;
@@ -43,44 +45,53 @@ enabled = true
.expect("persist remote plugin id");
let config = load_plugins_config(codex_home.path()).await;
let plugins_input = config.plugins_config_input();
let skills_service = Arc::new(HostSkillsService::new(
config.codex_home.clone(),
/*bundled_skills_enabled*/ false,
));
let plugins_manager = PluginsManager::new_with_options(
codex_home.path().to_path_buf(),
Some(Product::Codex),
Some(AuthMode::Chatgpt),
skills_service.clone(),
);
let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await;
write_file(&skill_path, "---\nname: search\ndescription: second\n---\n");
let skills_input = HostSkillsLoadInput::new(
config.cwd.clone(),
plugin_outcome.effective_plugin_skill_roots(),
config.config_layer_stack.clone(),
/*bundled_skills_enabled*/ false,
)
.with_plugin_skill_snapshots(plugins_manager.plugin_skill_snapshots_for_config(&plugins_input));
let skills_service = HostSkillsService::new(
config.codex_home.clone(),
/*bundled_skills_enabled*/ false,
);
let snapshot = skills_service
.snapshot_for_config(&skills_input, /*fs*/ None)
.await;
let other_cwd = codex_home.path().join("other-workspace");
std::fs::create_dir_all(&other_cwd).expect("create second workspace");
let other_cwd = AbsolutePathBuf::from_absolute_path(other_cwd).expect("absolute workspace");
assert_eq!(
snapshot
.outcome()
.skills
.iter()
.filter(|skill| skill.plugin_id.as_deref() == Some(PLUGIN_CONFIG_NAME))
.map(|skill| {
(
skill.description.as_str(),
skill.plugin_id.as_deref(),
skill.remote_plugin_id.as_deref(),
)
})
.collect::<Vec<_>>(),
vec![("first", Some(PLUGIN_CONFIG_NAME), Some(REMOTE_PLUGIN_ID),)]
);
for cwd in [config.cwd.clone(), other_cwd] {
let skills_input = HostSkillsLoadInput::new(
cwd,
plugin_outcome.effective_plugin_skill_roots(),
config.config_layer_stack.clone(),
/*bundled_skills_enabled*/ false,
)
.with_plugin_skill_snapshots(
plugins_manager.plugin_skill_snapshots_for_config(&plugins_input),
);
let snapshot = skills_service
.snapshot_for_config(&skills_input, /*fs*/ None)
.await;
assert_eq!(
snapshot
.outcome()
.skills
.iter()
.filter(|skill| skill.plugin_id.as_deref() == Some(PLUGIN_CONFIG_NAME))
.map(|skill| {
(
skill.description.as_str(),
skill.plugin_id.as_deref(),
skill.remote_plugin_id.as_deref(),
)
})
.collect::<Vec<_>>(),
vec![("first", Some(PLUGIN_CONFIG_NAME), Some(REMOTE_PLUGIN_ID),)]
);
}
}
+6 -5
View File
@@ -11,6 +11,7 @@ use crate::context::TurnAborted;
use crate::environment_selection::ThreadEnvironments;
use crate::environment_selection::TurnEnvironmentState;
use crate::function_tool::FunctionCallError;
use crate::plugins::plugins_manager_for_config;
use crate::session::step_context::StepContext;
use crate::shell::default_user_shell;
use crate::shell_snapshot::ShellSnapshot;
@@ -5567,7 +5568,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
let (tx_event, _rx_event) = async_channel::unbounded();
let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit);
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
let plugins_manager = Arc::new(plugins_manager_for_config(&config));
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
let skills_service = Arc::new(HostSkillsService::new(
config.codex_home.clone(),
@@ -5736,7 +5737,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
.expect("primary environment")
.environment,
);
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
let plugins_manager = Arc::new(plugins_manager_for_config(&config));
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
let skills_service = Arc::new(HostSkillsService::new(
config.codex_home.clone(),
@@ -5982,7 +5983,7 @@ async fn make_session_with_config_and_rx(
let (tx_event, rx_event) = async_channel::unbounded();
let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit);
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
let plugins_manager = Arc::new(plugins_manager_for_config(&config));
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
let skills_service = Arc::new(HostSkillsService::new(
config.codex_home.clone(),
@@ -6096,7 +6097,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
let (tx_event, rx_event) = async_channel::unbounded();
let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit);
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
let plugins_manager = Arc::new(plugins_manager_for_config(&config));
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
let skills_service = Arc::new(HostSkillsService::new(
config.codex_home.clone(),
@@ -7955,7 +7956,7 @@ where
.expect("primary environment")
.environment,
);
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
let plugins_manager = Arc::new(plugins_manager_for_config(&config));
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
let skills_service = Arc::new(HostSkillsService::new(
config.codex_home.clone(),
@@ -2,6 +2,7 @@ use super::*;
use crate::compact::InitialContextInjection;
use crate::exec_policy::ExecPolicyManager;
use crate::guardian::GUARDIAN_REVIEWER_NAME;
use crate::plugins::plugins_manager_for_config;
use crate::sandboxing::SandboxPermissions;
use crate::session::step_context::StepContext;
use crate::test_support::models_manager_with_provider;
@@ -738,7 +739,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
auth_manager.clone(),
config.model_provider.clone(),
);
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
let plugins_manager = Arc::new(plugins_manager_for_config(&config));
let skills_service = Arc::new(HostSkillsService::new(
config.codex_home.clone(),
/*bundled_skills_enabled*/ true,
+13 -11
View File
@@ -416,21 +416,22 @@ impl ThreadManager {
let codex_home = config.codex_home.clone();
let restriction_product = session_source.restriction_product();
let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY);
let skills_service = Arc::new(HostSkillsService::new_with_restriction_product(
codex_home.clone(),
config.bundled_skills_enabled(),
restriction_product,
));
let plugins_manager = Arc::new(PluginsManager::new_with_options(
codex_home.to_path_buf(),
restriction_product,
auth_manager.get_api_auth_mode(),
skills_service.clone(),
));
let mcp_manager = Arc::new(McpManager::new_with_extensions(
Arc::clone(&plugins_manager),
Arc::clone(&extensions),
codex_apps_tools_cache,
));
let skills_service = Arc::new(HostSkillsService::new_with_restriction_product(
codex_home,
config.bundled_skills_enabled(),
restriction_product,
));
let code_mode_session_provider: Arc<dyn CodeModeSessionProvider> =
if config.features.enabled(Feature::CodeModeHost)
|| config.code_mode.disable_in_process_fallback
@@ -562,17 +563,18 @@ impl ThreadManager {
};
let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY);
let restriction_product = SessionSource::Exec.restriction_product();
let plugins_manager = Arc::new(PluginsManager::new_with_options(
codex_home.clone(),
restriction_product,
auth_manager.get_api_auth_mode(),
));
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
let skills_service = Arc::new(HostSkillsService::new_with_restriction_product(
absolute_codex_home.clone(),
/*bundled_skills_enabled*/ true,
restriction_product,
));
let plugins_manager = Arc::new(PluginsManager::new_with_options(
codex_home.clone(),
restriction_product,
auth_manager.get_api_auth_mode(),
skills_service.clone(),
));
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
// This test constructor has no Config input. Tests that need a non-local
// process store should construct ThreadManager::new with an explicit store.
let thread_store: Arc<dyn ThreadStore> = Arc::new(LocalThreadStore::new(
@@ -1,4 +1,5 @@
use super::*;
use crate::plugins::plugins_manager_for_config;
use crate::plugins::test_support::load_plugins_config;
use crate::plugins::test_support::write_curated_plugin_sha;
use crate::plugins::test_support::write_openai_curated_marketplace;
@@ -10,7 +11,6 @@ use codex_config::types::ToolSuggestDisabledTool;
use codex_config::types::ToolSuggestDiscoverable;
use codex_config::types::ToolSuggestDiscoverableType;
use codex_core_plugins::PluginInstallRequest;
use codex_core_plugins::PluginsManager;
use codex_core_plugins::startup_sync::curated_plugins_repo_path;
use codex_rmcp_client::ElicitationResponse;
use codex_tools::DiscoverablePluginInfo;
@@ -40,7 +40,7 @@ async fn verified_plugin_install_completed_requires_installed_plugin() {
write_plugins_feature_config(codex_home.path());
let config = load_plugins_config(codex_home.path()).await;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let plugins_manager = plugins_manager_for_config(&config);
assert!(!verified_plugin_install_completed(
"sample@openai-curated",