mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4,6 +4,7 @@ use anyhow::Result;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::Constrained;
|
||||
use codex_core::plugins_manager_for_config;
|
||||
use codex_exec_server_test_support::environment_manager_without_environments;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -87,7 +88,7 @@ async fn hosted_plugin_runtime_ps_mcp_tool_calls_use_current_auth_manager_token(
|
||||
.build()
|
||||
.await?;
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never);
|
||||
let plugins_manager = codex_core_plugins::PluginsManager::new(home.path().to_path_buf());
|
||||
let plugins_manager = plugins_manager_for_config(&config);
|
||||
let mcp_config = Arc::new(config.to_mcp_config(&plugins_manager).await);
|
||||
let runtime = McpRuntime::new(McpRuntimeInput {
|
||||
startup_policy: McpStartupPolicy::Eager,
|
||||
|
||||
@@ -504,6 +504,111 @@ async fn agent_plugin_skills_use_shared_catalog_and_direct_child_discovery() ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case("CHATGPT", false, None; "product restricted skill is unavailable")]
|
||||
#[test_case("CODEX", true, Some("native review skill"); "native skill wins over migrated command")]
|
||||
#[test_case("CHATGPT", true, Some("migrated review command"); "migrated command replaces filtered native skill")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn plugin_skill_product_policy_and_migrated_command_precedence_reach_agent_turns(
|
||||
native_skill_product: &str,
|
||||
include_migrated_command: bool,
|
||||
expected_skill_description: Option<&str>,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let response = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
|
||||
)
|
||||
.await;
|
||||
let codex_home = Arc::new(TempDir::new()?);
|
||||
let plugin_root = write_sample_plugin_manifest_and_config(codex_home.as_ref());
|
||||
let native_skill_dir = plugin_root.join("skills/review");
|
||||
std::fs::create_dir_all(native_skill_dir.join("agents"))?;
|
||||
std::fs::write(
|
||||
native_skill_dir.join("SKILL.md"),
|
||||
"---\nname: source-command-review\ndescription: native review skill\n---\n",
|
||||
)?;
|
||||
std::fs::write(
|
||||
native_skill_dir.join("agents/openai.yaml"),
|
||||
format!("policy:\n products: [{native_skill_product}]\n"),
|
||||
)?;
|
||||
if include_migrated_command {
|
||||
let migrated_skill_dir =
|
||||
plugin_root.join(".codex-plugin/migrated-command-skills/source-command-review");
|
||||
std::fs::create_dir_all(&migrated_skill_dir)?;
|
||||
std::fs::write(
|
||||
migrated_skill_dir.join("SKILL.md"),
|
||||
"---\nname: source-command-review\ndescription: migrated review command\n---\n",
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_home(Arc::clone(&codex_home))
|
||||
.with_extensions(skills_extensions());
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
let plugin_outcome = test
|
||||
.thread_manager
|
||||
.plugins_manager()
|
||||
.plugins_for_config(&test.config.plugins_config_input())
|
||||
.await;
|
||||
assert_eq!(
|
||||
plugin_outcome
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|plugin| (plugin.config_name.as_str(), plugin.has_enabled_skills))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(
|
||||
SAMPLE_PLUGIN_CONFIG_NAME,
|
||||
expected_skill_description.is_some()
|
||||
)]
|
||||
);
|
||||
assert_eq!(
|
||||
plugin_outcome
|
||||
.capability_summaries()
|
||||
.iter()
|
||||
.map(|plugin| (plugin.config_name.as_str(), plugin.has_skills))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_skill_description
|
||||
.map(|_| (SAMPLE_PLUGIN_CONFIG_NAME, true))
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "Inspect the available plugin skills.".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
let developer_text = response
|
||||
.single_request()
|
||||
.message_input_texts("developer")
|
||||
.join("\n");
|
||||
assert_eq!(
|
||||
(
|
||||
developer_text.contains("sample:source-command-review: native review skill"),
|
||||
developer_text.contains("sample:source-command-review: migrated review command"),
|
||||
),
|
||||
(
|
||||
expected_skill_description == Some("native review skill"),
|
||||
expected_skill_description == Some("migrated review command"),
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn legacy_plugin_skill_prompt_remains_complete() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user