mirror of
https://github.com/openai/codex.git
synced 2026-10-20 08:19:50 +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:
@@ -4,9 +4,9 @@ use anyhow::bail;
|
||||
use clap::Parser;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::plugins_manager_for_config;
|
||||
use codex_core_plugins::PluginMarketplaceUpgradeOutcome;
|
||||
use codex_core_plugins::PluginsConfigInput;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
|
||||
use codex_core_plugins::installed_marketplaces::resolve_configured_marketplace_root;
|
||||
use codex_core_plugins::marketplace::marketplace_root_dir;
|
||||
@@ -211,7 +211,7 @@ async fn run_list(overrides: Vec<(String, toml::Value)>, args: ListMarketplaceAr
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let manager = PluginsManager::new(config.codex_home.to_path_buf());
|
||||
let manager = plugins_manager_for_config(&config);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await);
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let marketplace_listing = manager
|
||||
@@ -378,8 +378,7 @@ async fn run_upgrade(
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
||||
let manager = PluginsManager::new(codex_home.to_path_buf());
|
||||
let manager = plugins_manager_for_config(&config);
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let outcome = manager
|
||||
.upgrade_configured_marketplaces_for_config(&plugins_input, marketplace_name.as_deref())
|
||||
|
||||
@@ -17,7 +17,7 @@ use codex_core::config::LoaderOverrides;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::config::load_global_mcp_servers;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_core::plugins_manager_for_config;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::HttpClient;
|
||||
use codex_exec_server::RouteAwareHttpClient;
|
||||
@@ -478,7 +478,7 @@ async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveAr
|
||||
}
|
||||
|
||||
async fn load_mcp_manager(config: &Config) -> McpManager {
|
||||
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
|
||||
let plugins_manager = Arc::new(plugins_manager_for_config(config));
|
||||
plugins_manager.set_auth_mode(load_cli_auth_mode(config).await);
|
||||
McpManager::new(plugins_manager)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use anyhow::bail;
|
||||
use clap::Parser;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::plugins_manager_for_config;
|
||||
use codex_core_plugins::ConfiguredMarketplace;
|
||||
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::PluginInstallOutcome;
|
||||
@@ -589,7 +590,7 @@ async fn load_plugin_command_context(
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let manager = PluginsManager::new(codex_home.to_path_buf());
|
||||
let manager = plugins_manager_for_config(&config);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await);
|
||||
Ok(PluginCommandContext {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::remote::fetch_and_cache_global_remote_plugin_catalog;
|
||||
use crate::startup_sync::curated_plugins_repo_path;
|
||||
use crate::test_support::TEST_CURATED_PLUGIN_SHA;
|
||||
use crate::test_support::load_plugins_config;
|
||||
use crate::test_support::test_plugins_manager;
|
||||
use crate::test_support::write_curated_plugin;
|
||||
use crate::test_support::write_curated_plugin_sha_with;
|
||||
use crate::test_support::write_file;
|
||||
@@ -49,7 +50,7 @@ remote_plugin = false
|
||||
write_openai_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]);
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt));
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
@@ -78,7 +79,7 @@ async fn returns_api_curated_fallback_plugins_for_direct_provider_auth() {
|
||||
write_openai_api_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]);
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
plugins_manager.set_auth_mode(Some(AuthMode::ApiKey));
|
||||
let auth = CodexAuth::from_api_key("test-api-key");
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
@@ -111,7 +112,7 @@ async fn returns_microsoft_fallback_plugins() {
|
||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "teams").await;
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[], &[], &[]),
|
||||
@@ -169,7 +170,7 @@ source = "/tmp/{bundled_marketplace_name}"
|
||||
);
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt));
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
@@ -195,7 +196,7 @@ async fn includes_openai_curated_when_remote_enabled_without_auth() {
|
||||
write_openai_curated_marketplace(&curated_root, &["slack"]);
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[], &[], &[]),
|
||||
@@ -253,7 +254,7 @@ source = "/tmp/{marketplace_name}"
|
||||
),
|
||||
);
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
assert!(plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)));
|
||||
let chatgpt_projection = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
@@ -298,7 +299,7 @@ async fn reprojects_cached_skill_availability_for_current_config() {
|
||||
write_openai_curated_marketplace(&curated_root, &["slack"]);
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let expected = ToolSuggestDiscoverablePlugin {
|
||||
id: "slack@openai-curated".to_string(),
|
||||
remote_plugin_id: None,
|
||||
@@ -352,7 +353,7 @@ async fn does_not_advertise_skills_when_skill_loading_fails() {
|
||||
);
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[], &[], &[]),
|
||||
@@ -391,7 +392,7 @@ async fn clear_cache_invalidates_cached_tool_suggest_metadata() {
|
||||
);
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let input = discovery_input(plugins, &[], &[], &[]);
|
||||
let expected_cached = vec![ToolSuggestDiscoverablePlugin {
|
||||
id: "slack@openai-curated".to_string(),
|
||||
@@ -463,7 +464,7 @@ source = "/tmp/{marketplace_name}"
|
||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await;
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[], &[], &[]),
|
||||
@@ -490,7 +491,7 @@ async fn normalizes_description() {
|
||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await;
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[], &[], &[]),
|
||||
@@ -520,7 +521,7 @@ async fn omits_installed_curated_plugins() {
|
||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "slack").await;
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[], &[], &[]),
|
||||
@@ -574,7 +575,7 @@ async fn omits_not_available_curated_plugins() {
|
||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await;
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[], &[], &[]),
|
||||
@@ -615,7 +616,7 @@ async fn does_not_reload_marketplace_per_plugin() {
|
||||
}
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let buffer: &'static std::sync::Mutex<Vec<u8>> =
|
||||
Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
@@ -663,7 +664,7 @@ async fn does_not_expand_local_plugins_by_installed_apps() {
|
||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "slack").await;
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[], &[], &[]),
|
||||
@@ -689,7 +690,7 @@ async fn does_not_read_local_plugins_for_loaded_apps() {
|
||||
);
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let buffer: &'static std::sync::Mutex<Vec<u8>> =
|
||||
Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
@@ -775,7 +776,7 @@ source = "/tmp/{sales_marketplace_name}"
|
||||
install_marketplace_plugin(codex_home.path(), sales_marketplace_root.as_path(), "sales").await;
|
||||
|
||||
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let discoverable_plugins = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[], &[], &[]),
|
||||
@@ -894,7 +895,7 @@ plugins = true
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let mut plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
|
||||
plugins.chatgpt_base_url = format!("{}/backend-api", server.uri());
|
||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
fetch_and_cache_global_remote_plugin_catalog(
|
||||
codex_home.path(),
|
||||
&RemotePluginServiceConfig::new(
|
||||
@@ -1023,7 +1024,7 @@ fn string_set(values: &[&str]) -> HashSet<String> {
|
||||
async fn install_marketplace_plugin(codex_home: &Path, marketplace_root: &Path, plugin_name: &str) {
|
||||
write_curated_plugin_sha_with(codex_home, TEST_CURATED_PLUGIN_SHA);
|
||||
let config = load_plugins_config(codex_home, marketplace_root).await;
|
||||
PluginsManager::new(codex_home.to_path_buf())
|
||||
test_plugins_manager(codex_home.to_path_buf())
|
||||
.install_plugin(
|
||||
&config.config_layer_stack,
|
||||
PluginInstallRequest {
|
||||
|
||||
@@ -19,6 +19,7 @@ pub mod remote_bundle;
|
||||
pub mod remote_legacy;
|
||||
mod remote_plugin_id_resolver;
|
||||
mod script_attribution;
|
||||
mod skill_snapshots;
|
||||
pub mod startup_sync;
|
||||
pub mod store;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -28,7 +28,7 @@ use codex_config::types::PluginConfig;
|
||||
use codex_config::types::PluginMcpServerConfig;
|
||||
use codex_connectors::parse_plugin_app_config;
|
||||
use codex_connectors::parse_plugin_app_config_value;
|
||||
use codex_core_skills::PluginSkillSnapshots;
|
||||
use codex_core_skills::PluginSkillSnapshots as LegacyPluginSkillSnapshots;
|
||||
use codex_core_skills::config_rules::resolve_disabled_skill_paths;
|
||||
use codex_core_skills::config_rules::skill_config_rules_from_stack;
|
||||
use codex_core_skills::loader::SkillRoot;
|
||||
@@ -48,8 +48,12 @@ use codex_protocol::protocol::Product;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_skills::SkillConfigRules;
|
||||
use codex_skills::SkillMetadata;
|
||||
use codex_skills::SkillRootLoadRequest;
|
||||
use codex_skills::SkillRootLoader;
|
||||
use codex_skills::SkillRootSnapshots;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_plugins::PluginIdentity;
|
||||
use codex_utils_plugins::PluginSkillRoot;
|
||||
use codex_utils_plugins::SkillDiscoveryMode;
|
||||
use codex_utils_plugins::find_plugin_manifest_path;
|
||||
use codex_utils_plugins::migrated_command_skills_root;
|
||||
@@ -91,9 +95,9 @@ enum PluginLoadScope<'a> {
|
||||
AllCapabilities {
|
||||
restriction_product: Option<Product>,
|
||||
skill_config_rules: &'a SkillConfigRules,
|
||||
plugin_skill_snapshots: Option<&'a PluginSkillSnapshots>,
|
||||
plugin_skill_snapshots: Option<&'a SkillRootSnapshots<PluginSkillRoot>>,
|
||||
remote_plugin_id_resolver: &'a RemotePluginIdResolver,
|
||||
root_scan_slots: Arc<Semaphore>,
|
||||
skill_root_loader: &'a dyn SkillRootLoader<PluginSkillRoot>,
|
||||
},
|
||||
HooksOnly,
|
||||
}
|
||||
@@ -134,10 +138,10 @@ pub(crate) async fn load_plugins_from_layer_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
remote_installed_plugins_snapshot: RemoteInstalledPluginsSnapshot,
|
||||
store: &PluginStore,
|
||||
plugin_skill_snapshots: Option<&PluginSkillSnapshots>,
|
||||
plugin_skill_snapshots: Option<&SkillRootSnapshots<PluginSkillRoot>>,
|
||||
restriction_product: Option<Product>,
|
||||
remote_global_catalog_active: bool,
|
||||
root_scan_slots: Arc<Semaphore>,
|
||||
skill_root_loader: &dyn SkillRootLoader<PluginSkillRoot>,
|
||||
) -> Vec<LoadedPlugin<McpServerConfig>> {
|
||||
let skill_config_rules = skill_config_rules_from_stack(config_layer_stack);
|
||||
let RemoteInstalledPluginsSnapshot {
|
||||
@@ -154,7 +158,7 @@ pub(crate) async fn load_plugins_from_layer_stack(
|
||||
skill_config_rules: &skill_config_rules,
|
||||
plugin_skill_snapshots,
|
||||
remote_plugin_id_resolver: &remote_plugin_id_resolver,
|
||||
root_scan_slots,
|
||||
skill_root_loader,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -897,7 +901,7 @@ async fn load_plugin(
|
||||
skill_config_rules,
|
||||
plugin_skill_snapshots,
|
||||
remote_plugin_id_resolver: _,
|
||||
root_scan_slots,
|
||||
skill_root_loader,
|
||||
} => {
|
||||
loaded_plugin.manifest_name = Some(manifest.display_name().to_string());
|
||||
loaded_plugin.manifest_description = manifest.description.clone();
|
||||
@@ -907,14 +911,14 @@ async fn load_plugin(
|
||||
plugin_id: loaded_plugin_id.as_key(),
|
||||
remote_plugin_id: loaded_plugin.remote_plugin_id.clone(),
|
||||
};
|
||||
let resolved_skills = load_plugin_skill_inventory(
|
||||
let resolved_skills = load_plugin_skill_inventory_with_loader(
|
||||
&plugin_root,
|
||||
&plugin_identity,
|
||||
&manifest,
|
||||
loaded_manifest.format,
|
||||
*restriction_product,
|
||||
*plugin_skill_snapshots,
|
||||
Arc::clone(root_scan_slots),
|
||||
*skill_root_loader,
|
||||
)
|
||||
.await
|
||||
.resolve(skill_config_rules);
|
||||
@@ -1021,7 +1025,7 @@ pub async fn load_plugin_skills(
|
||||
manifest: &PluginManifest,
|
||||
restriction_product: Option<Product>,
|
||||
skill_config_rules: &SkillConfigRules,
|
||||
plugin_skill_snapshots: Option<&PluginSkillSnapshots>,
|
||||
plugin_skill_snapshots: Option<&LegacyPluginSkillSnapshots>,
|
||||
root_scan_slots: Arc<Semaphore>,
|
||||
) -> ResolvedPluginSkills {
|
||||
let plugin_identity = PluginIdentity {
|
||||
@@ -1046,7 +1050,7 @@ pub(crate) async fn load_plugin_skills_with_identity(
|
||||
manifest: &PluginManifest,
|
||||
restriction_product: Option<Product>,
|
||||
skill_config_rules: &SkillConfigRules,
|
||||
plugin_skill_snapshots: Option<&PluginSkillSnapshots>,
|
||||
plugin_skill_snapshots: Option<&LegacyPluginSkillSnapshots>,
|
||||
root_scan_slots: Arc<Semaphore>,
|
||||
) -> ResolvedPluginSkills {
|
||||
load_plugin_skill_inventory(
|
||||
@@ -1068,7 +1072,7 @@ pub(crate) async fn load_plugin_skill_inventory(
|
||||
manifest: &PluginManifest,
|
||||
manifest_format: PluginManifestFormat,
|
||||
restriction_product: Option<Product>,
|
||||
plugin_skill_snapshots: Option<&PluginSkillSnapshots>,
|
||||
plugin_skill_snapshots: Option<&LegacyPluginSkillSnapshots>,
|
||||
root_scan_slots: Arc<Semaphore>,
|
||||
) -> PluginSkillInventory {
|
||||
let discovery_mode = match manifest_format {
|
||||
@@ -1123,6 +1127,43 @@ pub(crate) async fn load_plugin_skill_inventory(
|
||||
PluginSkillInventory { skills, had_errors }
|
||||
}
|
||||
|
||||
pub(crate) async fn load_plugin_skill_inventory_with_loader(
|
||||
plugin_root: &AbsolutePathBuf,
|
||||
plugin_identity: &PluginIdentity,
|
||||
manifest: &PluginManifest,
|
||||
manifest_format: PluginManifestFormat,
|
||||
restriction_product: Option<Product>,
|
||||
plugin_skill_snapshots: Option<&SkillRootSnapshots<PluginSkillRoot>>,
|
||||
skill_root_loader: &dyn SkillRootLoader<PluginSkillRoot>,
|
||||
) -> PluginSkillInventory {
|
||||
let discovery_mode = match manifest_format {
|
||||
PluginManifestFormat::Legacy => SkillDiscoveryMode::Recursive,
|
||||
PluginManifestFormat::AgentPlugin => SkillDiscoveryMode::DirectChildren,
|
||||
};
|
||||
let roots = plugin_skill_roots(plugin_root, &manifest.paths, manifest_format)
|
||||
.into_iter()
|
||||
.map(|path| PluginSkillRoot {
|
||||
path,
|
||||
plugin_identity: plugin_identity.clone(),
|
||||
plugin_namespace: manifest.name.clone(),
|
||||
plugin_root: plugin_root.clone(),
|
||||
discovery_mode,
|
||||
})
|
||||
.collect();
|
||||
let outcome = skill_root_loader
|
||||
.load_roots(SkillRootLoadRequest {
|
||||
roots,
|
||||
restriction_product,
|
||||
snapshots: plugin_skill_snapshots.cloned(),
|
||||
})
|
||||
.await;
|
||||
|
||||
PluginSkillInventory {
|
||||
skills: outcome.skills,
|
||||
had_errors: !outcome.errors.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_skill_roots(
|
||||
plugin_root: &AbsolutePathBuf,
|
||||
manifest_paths: &PluginManifestPaths,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use super::*;
|
||||
use crate::manifest::load_plugin_manifest;
|
||||
use crate::manifest::load_plugin_manifest_with_format;
|
||||
use crate::test_support::test_skill_root_loader;
|
||||
use crate::test_support::write_file;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_core_skills::loader::MAX_CONCURRENT_ROOT_SCANS;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_utils_plugins::AGENT_PLUGIN_SCHEMA_URI;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -168,7 +168,7 @@ async fn installed_agent_plugin_uses_isolated_data_root_for_stdio_mcp() {
|
||||
/*plugin_skill_snapshots*/ None,
|
||||
Some(Product::Codex),
|
||||
/*remote_global_catalog_active*/ false,
|
||||
Arc::new(Semaphore::new(MAX_CONCURRENT_ROOT_SCANS)),
|
||||
test_skill_root_loader().as_ref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -337,7 +337,7 @@ enabled = true
|
||||
/*plugin_skill_snapshots*/ None,
|
||||
Some(Product::Codex),
|
||||
/*remote_global_catalog_active*/ false,
|
||||
Arc::new(Semaphore::new(MAX_CONCURRENT_ROOT_SCANS)),
|
||||
test_skill_root_loader().as_ref(),
|
||||
)
|
||||
.await;
|
||||
let hooks_only = load_plugins_from_layer_stack_with_scope(
|
||||
|
||||
@@ -58,6 +58,7 @@ use crate::remote_legacy::RemotePluginMutationError;
|
||||
use crate::remote_plugin_id_resolver::RemoteInstalledPluginsSnapshot;
|
||||
use crate::remote_plugin_id_resolver::RemotePluginIdResolver;
|
||||
use crate::remote_plugin_id_resolver::persisted_remote_plugin_id_for_installation;
|
||||
use crate::skill_snapshots::new_plugin_skill_snapshots;
|
||||
use crate::startup_sync::curated_plugins_api_marketplace_path;
|
||||
use crate::startup_sync::curated_plugins_repo_path;
|
||||
use crate::startup_sync::read_curated_plugins_sha;
|
||||
@@ -74,7 +75,6 @@ use codex_config::set_user_plugin_enabled;
|
||||
use codex_config::types::PluginConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
use codex_core_skills::PluginSkillSnapshots;
|
||||
use codex_core_skills::config_rules::skill_config_rules_from_stack;
|
||||
use codex_core_skills::loader::MAX_CONCURRENT_ROOT_SCANS;
|
||||
use codex_hooks::plugin_hook_declarations;
|
||||
@@ -94,6 +94,8 @@ use codex_protocol::protocol::HookEventName;
|
||||
use codex_protocol::protocol::Product;
|
||||
use codex_skills::SkillConfigRules;
|
||||
use codex_skills::SkillMetadata;
|
||||
use codex_skills::SkillRootLoader;
|
||||
use codex_skills::SkillRootSnapshots;
|
||||
use codex_tools::DiscoverablePluginInfo;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::filter_request_plugin_install_discoverable_tools_for_client;
|
||||
@@ -442,6 +444,7 @@ pub struct PluginsManager {
|
||||
loaded_plugins_cache: RwLock<LoadedPluginsCache>,
|
||||
loaded_plugins_load_semaphore: Semaphore,
|
||||
skill_root_scan_slots: Arc<Semaphore>,
|
||||
skill_root_loader: Arc<dyn SkillRootLoader<PluginSkillRoot>>,
|
||||
tool_suggest_metadata_cache: ToolSuggestMetadataCache,
|
||||
remote_installed_plugins_cache: RwLock<Option<Vec<RemoteInstalledPlugin>>>,
|
||||
remote_installed_plugins_cache_refresh_state: RwLock<RemoteInstalledPluginsCacheRefreshState>,
|
||||
@@ -456,7 +459,7 @@ pub struct PluginsManager {
|
||||
struct LoadedPluginsCacheEntry {
|
||||
key: PluginLoadCacheKey,
|
||||
plugins: Vec<LoadedPlugin>,
|
||||
plugin_skill_snapshots: PluginSkillSnapshots,
|
||||
plugin_skill_snapshots: SkillRootSnapshots<PluginSkillRoot>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -510,14 +513,23 @@ fn target_curated_marketplace(
|
||||
}
|
||||
|
||||
impl PluginsManager {
|
||||
pub fn new(codex_home: PathBuf) -> Self {
|
||||
Self::new_with_options(codex_home, Some(Product::Codex), /*auth_mode*/ None)
|
||||
pub fn new(
|
||||
codex_home: PathBuf,
|
||||
skill_root_loader: Arc<dyn SkillRootLoader<PluginSkillRoot>>,
|
||||
) -> Self {
|
||||
Self::new_with_options(
|
||||
codex_home,
|
||||
Some(Product::Codex),
|
||||
/*auth_mode*/ None,
|
||||
skill_root_loader,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_with_options(
|
||||
codex_home: PathBuf,
|
||||
restriction_product: Option<Product>,
|
||||
auth_mode: Option<AuthMode>,
|
||||
skill_root_loader: Arc<dyn SkillRootLoader<PluginSkillRoot>>,
|
||||
) -> Self {
|
||||
// Product restrictions are enforced at marketplace admission time for a given CODEX_HOME:
|
||||
// listing, install, and curated refresh all consult this restriction context before new
|
||||
@@ -544,6 +556,7 @@ impl PluginsManager {
|
||||
loaded_plugins_cache: RwLock::new(LoadedPluginsCache::default()),
|
||||
loaded_plugins_load_semaphore: Semaphore::new(/*permits*/ 1),
|
||||
skill_root_scan_slots: Arc::new(Semaphore::new(MAX_CONCURRENT_ROOT_SCANS)),
|
||||
skill_root_loader,
|
||||
tool_suggest_metadata_cache: ToolSuggestMetadataCache::new(),
|
||||
remote_installed_plugins_cache: RwLock::new(None),
|
||||
remote_installed_plugins_cache_refresh_state: RwLock::new(
|
||||
@@ -628,7 +641,7 @@ impl PluginsManager {
|
||||
pub fn plugin_skill_snapshots_for_config(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
) -> Option<PluginSkillSnapshots> {
|
||||
) -> Option<SkillRootSnapshots<PluginSkillRoot>> {
|
||||
if !config.plugins_enabled {
|
||||
return None;
|
||||
}
|
||||
@@ -683,7 +696,7 @@ impl PluginsManager {
|
||||
return self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id);
|
||||
}
|
||||
let cache_generation = self.loaded_plugins_cache_generation();
|
||||
let plugin_skill_snapshots = PluginSkillSnapshots::for_plugin_load();
|
||||
let plugin_skill_snapshots = new_plugin_skill_snapshots();
|
||||
let plugins = load_plugins_from_layer_stack(
|
||||
&config.config_layer_stack,
|
||||
self.remote_installed_plugins_snapshot(),
|
||||
@@ -691,7 +704,7 @@ impl PluginsManager {
|
||||
Some(&plugin_skill_snapshots),
|
||||
self.restriction_product,
|
||||
remote_global_catalog_active,
|
||||
Arc::clone(&self.skill_root_scan_slots),
|
||||
self.skill_root_loader.as_ref(),
|
||||
)
|
||||
.await;
|
||||
log_plugin_load_errors(&plugins);
|
||||
@@ -792,7 +805,7 @@ impl PluginsManager {
|
||||
/*plugin_skill_snapshots*/ None,
|
||||
self.restriction_product,
|
||||
self.remote_global_catalog_active(config),
|
||||
Arc::clone(&self.skill_root_scan_slots),
|
||||
self.skill_root_loader.as_ref(),
|
||||
)
|
||||
.await;
|
||||
self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id)
|
||||
@@ -852,7 +865,7 @@ impl PluginsManager {
|
||||
generation: u64,
|
||||
key: PluginLoadCacheKey,
|
||||
plugins: Vec<LoadedPlugin>,
|
||||
plugin_skill_snapshots: PluginSkillSnapshots,
|
||||
plugin_skill_snapshots: SkillRootSnapshots<PluginSkillRoot>,
|
||||
) {
|
||||
let mut cache = match self.loaded_plugins_cache.write() {
|
||||
Ok(cache) => cache,
|
||||
|
||||
@@ -21,6 +21,9 @@ use crate::test_support::TEST_CURATED_PLUGIN_CACHE_VERSION;
|
||||
use crate::test_support::TEST_CURATED_PLUGIN_SHA;
|
||||
use crate::test_support::load_plugins_config as load_plugins_config_input;
|
||||
use crate::test_support::test_http_client_factory;
|
||||
use crate::test_support::test_plugins_manager;
|
||||
use crate::test_support::test_plugins_manager_with_options;
|
||||
use crate::test_support::test_skill_root_loader;
|
||||
use crate::test_support::write_curated_plugin;
|
||||
use crate::test_support::write_curated_plugin_sha_with as write_curated_plugin_sha;
|
||||
use crate::test_support::write_file;
|
||||
@@ -40,7 +43,6 @@ use codex_config::RequirementSource;
|
||||
use codex_config::RequirementsLayerEntry;
|
||||
use codex_config::compose_requirements;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_core_skills::PluginSkillSnapshots;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_plugin::AppDeclaration;
|
||||
use codex_plugin::PluginId;
|
||||
@@ -118,7 +120,7 @@ fn plugins_config_input_with_requirements(
|
||||
#[test]
|
||||
fn plugins_manager_tracks_auth_mode() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
|
||||
assert_eq!(manager.auth_mode(), None);
|
||||
assert!(manager.set_auth_mode(Some(AuthMode::ApiKey)));
|
||||
@@ -129,7 +131,7 @@ fn plugins_manager_tracks_auth_mode() {
|
||||
assert!(manager.set_auth_mode(/*auth_mode*/ None));
|
||||
assert_eq!(manager.auth_mode(), None);
|
||||
|
||||
let manager_with_auth = PluginsManager::new_with_options(
|
||||
let manager_with_auth = test_plugins_manager_with_options(
|
||||
tmp.path().join("auth"),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -149,7 +151,7 @@ fn curated_repo_sync_stays_deferred_for_remote_chatgpt_catalog() {
|
||||
"https://chatgpt.com".to_string(),
|
||||
test_http_client_factory(),
|
||||
);
|
||||
let manager = Arc::new(PluginsManager::new_with_options(
|
||||
let manager = Arc::new(test_plugins_manager_with_options(
|
||||
tmp.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -165,7 +167,7 @@ fn curated_repo_sync_stays_deferred_for_remote_chatgpt_catalog() {
|
||||
#[test]
|
||||
fn marketplace_source_refresh_notifies_only_after_installed_cache_changes() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
let callback_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let callback_count_for_callback = Arc::clone(&callback_count);
|
||||
let callback: EffectivePluginsChangedCallback = Arc::new(move |_change| {
|
||||
@@ -225,7 +227,7 @@ source = "git"
|
||||
url = "https://github.com/example/other.git"
|
||||
"#,
|
||||
);
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
|
||||
let allowed_outcome = manager.plugins_for_config(&allowed).await;
|
||||
assert_eq!(allowed_outcome.plugins().len(), 1);
|
||||
@@ -264,7 +266,7 @@ restrict_to_allowed_sources = true
|
||||
AbsolutePathBuf::try_from(marketplace_root.join(".agents/plugins/marketplace.json"))
|
||||
.expect("absolute marketplace path");
|
||||
|
||||
let err = PluginsManager::new(codex_home.path().to_path_buf())
|
||||
let err = test_plugins_manager(codex_home.path().to_path_buf())
|
||||
.read_plugin_for_config(
|
||||
&config,
|
||||
&PluginReadRequest {
|
||||
@@ -303,7 +305,7 @@ fn marketplace_policy_filters_discovered_marketplaces_by_configured_name() {
|
||||
let repo_root = AbsolutePathBuf::try_from(repo_root).expect("absolute repository root");
|
||||
let subdirectory =
|
||||
AbsolutePathBuf::try_from(subdirectory).expect("absolute input subdirectory");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let user_config = format!(
|
||||
r#"
|
||||
[marketplaces.company]
|
||||
@@ -442,7 +444,7 @@ async fn plugin_auth_projection_hides_apps_without_chatgpt_auth() {
|
||||
write_auth_projection_plugin(codex_home.path(), "sample", /*include_app*/ true);
|
||||
write_auth_projection_plugin(codex_home.path(), "docs", /*include_app*/ false);
|
||||
let config = auth_projection_config(codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::ApiKey),
|
||||
@@ -470,7 +472,7 @@ async fn plugin_auth_projection_hides_matching_mcp_with_chatgpt_apps_route() {
|
||||
write_auth_projection_plugin(codex_home.path(), "sample", /*include_app*/ true);
|
||||
write_auth_projection_plugin(codex_home.path(), "docs", /*include_app*/ false);
|
||||
let config = auth_projection_config(codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -511,7 +513,7 @@ async fn plugin_auth_projection_hides_dual_surface_mcp_with_agent_identity_apps_
|
||||
write_auth_projection_plugin(codex_home.path(), "sample", /*include_app*/ true);
|
||||
write_auth_projection_plugin(codex_home.path(), "docs", /*include_app*/ false);
|
||||
let config = auth_projection_config(codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::AgentIdentity),
|
||||
@@ -536,7 +538,7 @@ async fn plugin_auth_projection_keeps_non_conflicting_mcp_with_chatgpt_apps_rout
|
||||
write_auth_projection_app(codex_home.path(), "sample", "sample_app");
|
||||
write_auth_projection_plugin(codex_home.path(), "docs", /*include_app*/ false);
|
||||
let config = auth_projection_config(codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -619,7 +621,7 @@ enabled = true
|
||||
"#,
|
||||
);
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -653,7 +655,7 @@ async fn plugin_auth_projection_reprojects_cached_plugins_when_auth_changes() {
|
||||
write_auth_projection_plugin(codex_home.path(), "sample", /*include_app*/ true);
|
||||
write_auth_projection_plugin(codex_home.path(), "docs", /*include_app*/ false);
|
||||
let config = auth_projection_config(codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -811,7 +813,7 @@ async fn load_plugins_from_config(
|
||||
) -> PluginLoadOutcome {
|
||||
write_file(&codex_home.join(CONFIG_TOML_FILE), config_toml);
|
||||
let config = load_config(codex_home, codex_home).await;
|
||||
PluginsManager::new_with_options(codex_home.to_path_buf(), Some(Product::Codex), auth_mode)
|
||||
test_plugins_manager_with_options(codex_home.to_path_buf(), Some(Product::Codex), auth_mode)
|
||||
.plugins_for_config(&config)
|
||||
.await
|
||||
}
|
||||
@@ -1172,7 +1174,7 @@ approval_mode = "approve"
|
||||
);
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -1215,7 +1217,7 @@ plugins = true
|
||||
);
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
manager.write_remote_installed_plugins_cache(vec![remote_installed_linear_plugin()]);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
@@ -1226,7 +1228,7 @@ plugins = true
|
||||
async fn installed_plugin_telemetry_metadata_collects_capabilities() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_cached_plugin(codex_home.path(), "test", "sample");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let plugin_id = PluginId::parse("sample@test").expect("plugin id should parse");
|
||||
|
||||
let metadata = manager
|
||||
@@ -1260,7 +1262,7 @@ async fn installed_plugin_telemetry_metadata_resolves_persisted_remote_identity(
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_linear")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
|
||||
let metadata = manager
|
||||
.telemetry_metadata_for_installed_plugin(&plugin_id)
|
||||
@@ -1292,7 +1294,7 @@ fn plugin_telemetry_ignores_local_marketplace_sidecars() {
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_sample")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
|
||||
assert_eq!(
|
||||
manager.telemetry_metadata_for_plugin_id(&plugin_id),
|
||||
@@ -1313,7 +1315,7 @@ async fn installed_plugin_telemetry_metadata_prefers_remote_snapshot_identity()
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_stale")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
manager.write_remote_installed_plugins_cache(vec![remote_installed_linear_plugin()]);
|
||||
|
||||
let metadata = manager
|
||||
@@ -1341,7 +1343,7 @@ async fn installed_plugin_telemetry_metadata_prefers_remote_snapshot_identity()
|
||||
#[tokio::test]
|
||||
async fn installed_plugin_telemetry_metadata_accepts_authoritative_remote_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let plugin_id =
|
||||
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
|
||||
|
||||
@@ -1362,7 +1364,7 @@ async fn installed_plugin_telemetry_metadata_accepts_authoritative_remote_identi
|
||||
#[test]
|
||||
fn capability_summary_telemetry_metadata_uses_local_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let summary = PluginCapabilitySummary {
|
||||
config_name: "linear@openai-curated-remote".to_string(),
|
||||
display_name: "Linear".to_string(),
|
||||
@@ -1396,7 +1398,7 @@ fn capability_summary_telemetry_metadata_resolves_persisted_remote_identity() {
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_linear")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let summary = PluginCapabilitySummary {
|
||||
config_name: "linear@openai-curated-remote".to_string(),
|
||||
display_name: "Linear".to_string(),
|
||||
@@ -1445,7 +1447,7 @@ enabled = true
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "remote-only");
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -1482,7 +1484,7 @@ enabled = true
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -1520,7 +1522,7 @@ enabled = true
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "remote-only");
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -1559,7 +1561,7 @@ enabled = true
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "remote-only");
|
||||
|
||||
let mut config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -1602,7 +1604,7 @@ enabled = true
|
||||
#[tokio::test]
|
||||
async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metadata() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let mut plugin = remote_installed_linear_plugin();
|
||||
plugin.install_policy = codex_app_server_protocol::PluginInstallPolicy::InstalledByDefault;
|
||||
plugin.auth_policy = codex_app_server_protocol::PluginAuthPolicy::OnInstall;
|
||||
@@ -1679,7 +1681,7 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad
|
||||
#[tokio::test]
|
||||
async fn build_remote_installed_plugin_marketplaces_from_cache_filters_by_marketplace_name() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
manager.write_remote_installed_plugins_cache(vec![
|
||||
remote_installed_plugin_in_marketplace(
|
||||
"workspace-linear",
|
||||
@@ -2798,7 +2800,7 @@ async fn load_plugins_returns_empty_when_feature_disabled() {
|
||||
);
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let outcome = PluginsManager::new(codex_home.path().to_path_buf())
|
||||
let outcome = test_plugins_manager(codex_home.path().to_path_buf())
|
||||
.plugins_for_config(&config)
|
||||
.await;
|
||||
|
||||
@@ -2863,7 +2865,7 @@ async fn plugin_cache_ignores_unrelated_session_overrides() {
|
||||
test_http_client_factory(),
|
||||
)
|
||||
};
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
|
||||
let first = manager
|
||||
.plugins_for_config(&config(r#"model = "first""#))
|
||||
@@ -2923,7 +2925,7 @@ enabled = true
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_persisted")
|
||||
.expect("persist remote plugin id");
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -2962,7 +2964,7 @@ enabled = true
|
||||
#[test]
|
||||
fn loaded_plugins_cache_invalidation_rejects_stale_load_completion() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
let cache_key = PluginLoadCacheKey {
|
||||
configured_plugins: HashMap::new(),
|
||||
skill_config_rules: SkillConfigRules::default(),
|
||||
@@ -2975,7 +2977,7 @@ fn loaded_plugins_cache_invalidation_rejects_stale_load_completion() {
|
||||
stale_generation,
|
||||
cache_key.clone(),
|
||||
Vec::new(),
|
||||
PluginSkillSnapshots::for_plugin_load(),
|
||||
crate::skill_snapshots::new_plugin_skill_snapshots(),
|
||||
);
|
||||
|
||||
assert_eq!(manager.cached_loaded_plugins(&cache_key), None);
|
||||
@@ -3049,7 +3051,7 @@ async fn install_plugin_updates_config_with_relative_path_and_plugin_key() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let result = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.install_plugin(
|
||||
&unrestricted_config_layer_stack(),
|
||||
PluginInstallRequest {
|
||||
@@ -3113,7 +3115,7 @@ path = {marketplace_root:?}
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(marketplace_root.join(".agents/plugins/marketplace.json"))
|
||||
.expect("absolute marketplace path");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = test_plugins_manager(codex_home.path().to_path_buf());
|
||||
|
||||
let err = manager
|
||||
.install_plugin(
|
||||
@@ -3165,7 +3167,7 @@ async fn install_openai_curated_plugin_uses_short_sha_cache_version() {
|
||||
write_openai_curated_marketplace(&curated_root, &["slack"]);
|
||||
write_curated_plugin_sha(tmp.path(), TEST_CURATED_PLUGIN_SHA);
|
||||
|
||||
let result = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let result = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.install_plugin(
|
||||
&unrestricted_config_layer_stack(),
|
||||
PluginInstallRequest {
|
||||
@@ -3226,7 +3228,7 @@ async fn install_plugin_uses_manifest_version_for_non_curated_plugins() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let result = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.install_plugin(
|
||||
&unrestricted_config_layer_stack(),
|
||||
PluginInstallRequest {
|
||||
@@ -3294,7 +3296,7 @@ async fn install_plugin_writes_marketplace_manifest_fallback_when_missing_plugin
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let result = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.install_plugin(
|
||||
&unrestricted_config_layer_stack(),
|
||||
PluginInstallRequest {
|
||||
@@ -3394,7 +3396,7 @@ async fn install_plugin_supports_git_subdir_marketplace_sources() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let result = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.install_plugin(
|
||||
&unrestricted_config_layer_stack(),
|
||||
PluginInstallRequest {
|
||||
@@ -3448,7 +3450,7 @@ async fn install_plugin_supports_relative_git_subdir_marketplace_sources() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let result = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.install_plugin(
|
||||
&unrestricted_config_layer_stack(),
|
||||
PluginInstallRequest {
|
||||
@@ -3493,7 +3495,7 @@ enabled = true
|
||||
"#,
|
||||
);
|
||||
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
manager
|
||||
.uninstall_plugin("sample-plugin@debug".to_string())
|
||||
.await
|
||||
@@ -3565,7 +3567,7 @@ enabled = false
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(
|
||||
&config,
|
||||
&[AbsolutePathBuf::try_from(repo_root).unwrap()],
|
||||
@@ -3673,7 +3675,7 @@ enabled = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(
|
||||
&config,
|
||||
&[AbsolutePathBuf::try_from(repo_root).unwrap()],
|
||||
@@ -3725,7 +3727,7 @@ plugins = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(
|
||||
&config,
|
||||
&[AbsolutePathBuf::try_from(repo_root).unwrap()],
|
||||
@@ -3803,7 +3805,7 @@ enabled = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let err = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let err = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.read_plugin_for_config(
|
||||
&config,
|
||||
&PluginReadRequest {
|
||||
@@ -3866,7 +3868,7 @@ plugins = true
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
let chatgpt_outcome = PluginsManager::new_with_options(
|
||||
let chatgpt_outcome = test_plugins_manager_with_options(
|
||||
tmp.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -3883,7 +3885,7 @@ plugins = true
|
||||
vec![AppConnectorId("connector_sample".to_string())]
|
||||
);
|
||||
|
||||
let api_key_outcome = PluginsManager::new_with_options(
|
||||
let api_key_outcome = test_plugins_manager_with_options(
|
||||
tmp.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::ApiKey),
|
||||
@@ -3935,7 +3937,7 @@ plugins = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
let outcome = manager
|
||||
.read_plugin_for_config(
|
||||
&config,
|
||||
@@ -4037,7 +4039,7 @@ async fn agent_plugin_read_and_tool_suggestions_use_portable_capabilities_only()
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
let plugin = manager
|
||||
.list_marketplaces_for_config(
|
||||
&config,
|
||||
@@ -4111,7 +4113,7 @@ plugins = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let err = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let err = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.read_plugin_for_config(
|
||||
&config,
|
||||
&PluginReadRequest {
|
||||
@@ -4176,7 +4178,7 @@ enabled = false
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let outcome = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let outcome = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.read_plugin_for_config(
|
||||
&config,
|
||||
&PluginReadRequest {
|
||||
@@ -4232,7 +4234,7 @@ plugins = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let outcome = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let outcome = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.read_plugin_for_config(
|
||||
&config,
|
||||
&PluginReadRequest {
|
||||
@@ -4375,7 +4377,7 @@ enabled = false
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let outcome = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let outcome = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.read_plugin_for_config(
|
||||
&config,
|
||||
&PluginReadRequest {
|
||||
@@ -4496,7 +4498,7 @@ enabled = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(
|
||||
&config,
|
||||
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
|
||||
@@ -4602,7 +4604,7 @@ plugins = true
|
||||
.unwrap();
|
||||
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
.unwrap()
|
||||
.marketplaces;
|
||||
@@ -4659,7 +4661,7 @@ plugins = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let outcome = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let outcome = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ false)
|
||||
.unwrap();
|
||||
|
||||
@@ -4719,7 +4721,7 @@ plugins = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
manager.set_auth_mode(Some(AuthMode::ApiKey));
|
||||
let marketplaces = manager
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
@@ -4797,7 +4799,7 @@ plugins = true
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.model_provider_id = resolved_provider.to_string();
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
.unwrap()
|
||||
.marketplaces;
|
||||
@@ -4828,7 +4830,7 @@ plugins = true
|
||||
write_openai_api_curated_marketplace(&curated_root, &["api-plugin"]);
|
||||
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
manager.set_auth_mode(Some(AuthMode::Chatgpt));
|
||||
let marketplaces = manager
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
@@ -4864,7 +4866,7 @@ plugins = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
manager.set_auth_mode(Some(AuthMode::BedrockApiKey));
|
||||
let outcome = manager
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
@@ -4921,7 +4923,7 @@ source = "/tmp/debug"
|
||||
)
|
||||
.unwrap();
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
.unwrap()
|
||||
.marketplaces;
|
||||
@@ -4993,7 +4995,7 @@ source = "{remote_repo_url}"
|
||||
),
|
||||
);
|
||||
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let initial_upgrade = manager
|
||||
.upgrade_configured_marketplaces_for_config(&config, /*marketplace_name*/ None)
|
||||
@@ -5094,7 +5096,7 @@ source = "/tmp/debug"
|
||||
fs::write(registry_path, "{not valid json").unwrap();
|
||||
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
.unwrap()
|
||||
.marketplaces;
|
||||
@@ -5149,7 +5151,7 @@ plugins = true
|
||||
)
|
||||
.unwrap();
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
.unwrap()
|
||||
.marketplaces;
|
||||
@@ -5228,7 +5230,7 @@ enabled = false
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_a_root).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(
|
||||
&config,
|
||||
&[
|
||||
@@ -5347,7 +5349,7 @@ enabled = true
|
||||
);
|
||||
|
||||
let config = load_config(tmp.path(), &repo_root).await;
|
||||
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
|
||||
let marketplaces = test_plugins_manager(tmp.path().to_path_buf())
|
||||
.list_marketplaces_for_config(
|
||||
&config,
|
||||
&[AbsolutePathBuf::try_from(repo_root).unwrap()],
|
||||
@@ -5421,7 +5423,7 @@ plugins = true
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = format!("{}/backend-api/", server.uri());
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
tmp.path().to_path_buf(),
|
||||
Some(Product::Chatgpt),
|
||||
/*auth_mode*/ None,
|
||||
@@ -5458,7 +5460,7 @@ plugins = true
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = format!("{}/backend-api/", server.uri());
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
tmp.path().to_path_buf(),
|
||||
/*restriction_product*/ None,
|
||||
/*auth_mode*/ None,
|
||||
@@ -5496,7 +5498,7 @@ plugins = true
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = std::sync::Arc::new(PluginsManager::new(tmp.path().to_path_buf()));
|
||||
let manager = std::sync::Arc::new(test_plugins_manager(tmp.path().to_path_buf()));
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let cache_key = recommended_plugins_cache_key(&config);
|
||||
|
||||
@@ -5577,7 +5579,7 @@ plugins = true
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let expected = RecommendedPluginsMode::Endpoint {
|
||||
plugins: vec![
|
||||
@@ -5647,7 +5649,7 @@ plugins = true
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
let mut installed_linear = remote_installed_plugin("linear");
|
||||
installed_linear.id = "plugin_linear".to_string();
|
||||
manager.write_remote_installed_plugins_cache(vec![installed_linear]);
|
||||
@@ -5704,7 +5706,7 @@ plugins = true
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
assert_eq!(
|
||||
manager
|
||||
@@ -5740,7 +5742,7 @@ plugins = true
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let manager = test_plugins_manager(tmp.path().to_path_buf());
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
assert_eq!(
|
||||
manager
|
||||
@@ -6424,7 +6426,7 @@ async fn load_plugins_ignores_project_config_files() {
|
||||
/*plugin_skill_snapshots*/ None,
|
||||
Some(Product::Codex),
|
||||
/*remote_global_catalog_active*/ false,
|
||||
Arc::new(Semaphore::new(MAX_CONCURRENT_ROOT_SCANS)),
|
||||
test_skill_root_loader().as_ref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -6466,7 +6468,7 @@ async fn plugin_hooks_for_layer_stack_loads_configured_plugin_hooks() {
|
||||
);
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
|
||||
let outcome = PluginsManager::new(codex_home.path().to_path_buf())
|
||||
let outcome = test_plugins_manager(codex_home.path().to_path_buf())
|
||||
.plugin_hooks_for_layer_stack(&config.config_layer_stack, &config)
|
||||
.await;
|
||||
|
||||
@@ -6524,7 +6526,7 @@ enabled = true
|
||||
);
|
||||
}
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
let manager = test_plugins_manager_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
@@ -6572,7 +6574,7 @@ enabled = true
|
||||
#[test]
|
||||
fn remote_installed_plugins_cache_refresh_coalesces_materializations() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let manager = std::sync::Arc::new(PluginsManager::new(tmp.path().to_path_buf()));
|
||||
let manager = std::sync::Arc::new(test_plugins_manager(tmp.path().to_path_buf()));
|
||||
let materialization_callback_count =
|
||||
std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let unrelated_callback_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use codex_skills::LoadedSkillRoot;
|
||||
use codex_skills::SkillRootSnapshotCache;
|
||||
use codex_skills::SkillRootSnapshots;
|
||||
use codex_utils_plugins::PluginSkillRoot;
|
||||
|
||||
#[derive(Default)]
|
||||
struct PluginSkillSnapshotCache {
|
||||
snapshots_by_root: Mutex<HashMap<PluginSkillRoot, LoadedSkillRoot>>,
|
||||
}
|
||||
|
||||
impl SkillRootSnapshotCache<PluginSkillRoot> for PluginSkillSnapshotCache {
|
||||
fn get(&self, root: &PluginSkillRoot) -> Option<LoadedSkillRoot> {
|
||||
self.snapshots_by_root
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(root)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn insert(&self, root: PluginSkillRoot, snapshot: LoadedSkillRoot) {
|
||||
self.snapshots_by_root
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(root, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_plugin_skill_snapshots() -> SkillRootSnapshots<PluginSkillRoot> {
|
||||
SkillRootSnapshots::new(Arc::new(PluginSkillSnapshotCache::default()))
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::OPENAI_API_CURATED_MARKETPLACE_NAME;
|
||||
use crate::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use crate::PluginsConfigInput;
|
||||
use crate::PluginsManager;
|
||||
use crate::http_client_selector::HttpClientSelector;
|
||||
use crate::remote::RemotePluginServiceConfig;
|
||||
use codex_config::LoaderOverrides;
|
||||
@@ -17,13 +21,194 @@ use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_http_client::RouteAwareClientPool;
|
||||
use codex_http_client::RouteAwareRequestBuilder;
|
||||
use codex_protocol::auth::AuthMode;
|
||||
use codex_protocol::protocol::Product;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_skills::LoadedSkillRoot;
|
||||
use codex_skills::LoadedSkills;
|
||||
use codex_skills::SkillError;
|
||||
use codex_skills::SkillLoadFuture;
|
||||
use codex_skills::SkillMetadata;
|
||||
use codex_skills::SkillRootLoadRequest;
|
||||
use codex_skills::SkillRootLoader;
|
||||
use codex_skills::parse_skill_frontmatter_metadata;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_plugins::PluginSkillRoot;
|
||||
use codex_utils_plugins::SkillDiscoveryMode;
|
||||
use codex_utils_plugins::migrated_command_skills_root;
|
||||
use http::Method;
|
||||
use toml::Value;
|
||||
|
||||
pub(crate) const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
|
||||
pub(crate) const TEST_CURATED_PLUGIN_CACHE_VERSION: &str = "01234567";
|
||||
|
||||
pub(crate) fn test_plugins_manager(codex_home: PathBuf) -> PluginsManager {
|
||||
PluginsManager::new(codex_home, test_skill_root_loader())
|
||||
}
|
||||
|
||||
pub(crate) fn test_plugins_manager_with_options(
|
||||
codex_home: PathBuf,
|
||||
restriction_product: Option<Product>,
|
||||
auth_mode: Option<AuthMode>,
|
||||
) -> PluginsManager {
|
||||
PluginsManager::new_with_options(
|
||||
codex_home,
|
||||
restriction_product,
|
||||
auth_mode,
|
||||
test_skill_root_loader(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn test_skill_root_loader() -> Arc<dyn SkillRootLoader<PluginSkillRoot>> {
|
||||
Arc::new(TestSkillRootLoader)
|
||||
}
|
||||
|
||||
struct TestSkillRootLoader;
|
||||
|
||||
impl SkillRootLoader<PluginSkillRoot> for TestSkillRootLoader {
|
||||
fn load_roots(
|
||||
&self,
|
||||
request: SkillRootLoadRequest<PluginSkillRoot>,
|
||||
) -> SkillLoadFuture<'_, LoadedSkills> {
|
||||
Box::pin(async move {
|
||||
let mut loaded_roots = Vec::new();
|
||||
for root in request.roots {
|
||||
let cached = request
|
||||
.snapshots
|
||||
.as_ref()
|
||||
.and_then(|cache| cache.get(&root));
|
||||
let snapshot = match cached {
|
||||
Some(snapshot) => snapshot,
|
||||
None => {
|
||||
let snapshot = load_test_skill_root(&root);
|
||||
if let Some(snapshots) = &request.snapshots {
|
||||
snapshots.insert(root.clone(), snapshot.clone());
|
||||
}
|
||||
snapshot
|
||||
}
|
||||
};
|
||||
let migrated_root = migrated_command_skills_root(&root.plugin_root);
|
||||
let canonical_migrated_root = fs::canonicalize(migrated_root.as_path())
|
||||
.ok()
|
||||
.and_then(|path| AbsolutePathBuf::from_absolute_path_checked(path).ok())
|
||||
.unwrap_or(migrated_root);
|
||||
loaded_roots.push((snapshot.root == canonical_migrated_root, snapshot));
|
||||
}
|
||||
|
||||
let native_names = loaded_roots
|
||||
.iter()
|
||||
.filter(|(migrated, _)| !migrated)
|
||||
.flat_map(|(_, snapshot)| &snapshot.skills)
|
||||
.map(|skill| (skill.plugin_id.clone(), skill.name.clone()))
|
||||
.collect::<HashSet<_>>();
|
||||
let mut seen_paths = HashSet::new();
|
||||
let mut outcome = LoadedSkills::default();
|
||||
for (migrated, snapshot) in loaded_roots {
|
||||
outcome
|
||||
.skills
|
||||
.extend(snapshot.skills.into_iter().filter(|skill| {
|
||||
(!migrated
|
||||
|| !native_names
|
||||
.contains(&(skill.plugin_id.clone(), skill.name.clone())))
|
||||
&& skill.matches_product_restriction_for_product(
|
||||
request.restriction_product,
|
||||
)
|
||||
&& seen_paths.insert(skill.path_to_skills_md.clone())
|
||||
}));
|
||||
outcome.errors.extend(snapshot.errors);
|
||||
}
|
||||
outcome.skills.sort_by(|left, right| {
|
||||
left.name
|
||||
.cmp(&right.name)
|
||||
.then_with(|| left.path_to_skills_md.cmp(&right.path_to_skills_md))
|
||||
});
|
||||
outcome
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn load_test_skill_root(root: &PluginSkillRoot) -> LoadedSkillRoot {
|
||||
let canonical_root = fs::canonicalize(root.path.as_path())
|
||||
.ok()
|
||||
.and_then(|path| AbsolutePathBuf::from_absolute_path_checked(path).ok())
|
||||
.unwrap_or_else(|| root.path.clone());
|
||||
let mut skills = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
let mut discovery_paths = HashMap::new();
|
||||
let mut directories = vec![root.path.clone()];
|
||||
while let Some(directory) = directories.pop() {
|
||||
let Ok(entries) = fs::read_dir(directory.as_path()) else {
|
||||
continue;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if (root.discovery_mode == SkillDiscoveryMode::Recursive || directory == root.path)
|
||||
&& let Ok(path) = AbsolutePathBuf::from_absolute_path_checked(path)
|
||||
{
|
||||
directories.push(path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if path.file_name().is_none_or(|name| name != "SKILL.md")
|
||||
|| (root.discovery_mode == SkillDiscoveryMode::DirectChildren
|
||||
&& directory == root.path)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Ok(path) = AbsolutePathBuf::from_absolute_path_checked(path) else {
|
||||
continue;
|
||||
};
|
||||
let canonical_path = fs::canonicalize(path.as_path())
|
||||
.ok()
|
||||
.and_then(|path| AbsolutePathBuf::from_absolute_path_checked(path).ok())
|
||||
.unwrap_or_else(|| path.clone());
|
||||
let parsed = fs::read_to_string(path.as_path())
|
||||
.map_err(|error| error.to_string())
|
||||
.and_then(|contents| {
|
||||
parse_skill_frontmatter_metadata(&contents, || {
|
||||
directory
|
||||
.as_path()
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
})
|
||||
.map_err(|error| error.to_string())
|
||||
});
|
||||
match parsed {
|
||||
Ok(parsed) => {
|
||||
discovery_paths.insert(canonical_path.clone(), path);
|
||||
skills.push(SkillMetadata {
|
||||
name: format!("{}:{}", root.plugin_namespace, parsed.name),
|
||||
description: parsed.description,
|
||||
short_description: parsed.short_description,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: canonical_path,
|
||||
scope: SkillScope::User,
|
||||
plugin_id: Some(root.plugin_identity.plugin_id.clone()),
|
||||
remote_plugin_id: root.plugin_identity.remote_plugin_id.clone(),
|
||||
});
|
||||
}
|
||||
Err(message) => errors.push(SkillError {
|
||||
path: canonical_path,
|
||||
message,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LoadedSkillRoot {
|
||||
root: canonical_root,
|
||||
skills,
|
||||
skill_discovery_path_by_path: Arc::new(discovery_paths),
|
||||
errors,
|
||||
is_agent_plugin: root.discovery_mode == SkillDiscoveryMode::DirectChildren,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn test_http_client_factory() -> HttpClientFactory {
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault)
|
||||
}
|
||||
|
||||
@@ -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(()));
|
||||
|
||||
@@ -4,7 +4,7 @@ use codex_config::McpServerTransportConfig;
|
||||
use codex_core::McpManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_core::plugins_manager_for_config;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::McpServerContribution;
|
||||
use codex_extension_api::McpServerContributionContext;
|
||||
@@ -87,9 +87,7 @@ async fn default_fallback_overwrites_reserved_config_without_an_extension() -> T
|
||||
.build()
|
||||
.await?;
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let manager = McpManager::new(Arc::new(PluginsManager::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
)));
|
||||
let manager = McpManager::new(Arc::new(plugins_manager_for_config(&config)));
|
||||
|
||||
let servers = manager.effective_servers(&config, Some(&auth)).await;
|
||||
let server = servers
|
||||
@@ -118,7 +116,7 @@ async fn later_extension_can_remove_same_name_registration() -> TestResult {
|
||||
codex_mcp_extension::install(&mut builder);
|
||||
builder.mcp_server_contributor(Arc::new(RemoveCodexApps));
|
||||
let manager = McpManager::new_with_extensions(
|
||||
Arc::new(PluginsManager::new(config.codex_home.to_path_buf())),
|
||||
Arc::new(plugins_manager_for_config(&config)),
|
||||
Arc::new(builder.build()),
|
||||
codex_core::CodexAppsToolsCache::default(),
|
||||
);
|
||||
@@ -164,9 +162,7 @@ async fn disabled_apps_remove_reserved_server_config_for_all_hosts() -> TestResu
|
||||
.await?;
|
||||
let managers = [
|
||||
installed_manager(&config),
|
||||
McpManager::new(Arc::new(PluginsManager::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
))),
|
||||
McpManager::new(Arc::new(plugins_manager_for_config(&config))),
|
||||
];
|
||||
for manager in managers {
|
||||
let servers = manager.runtime_servers(&config).await;
|
||||
@@ -179,7 +175,7 @@ fn installed_manager(config: &Config) -> McpManager {
|
||||
let mut builder = ExtensionRegistryBuilder::new();
|
||||
codex_mcp_extension::install(&mut builder);
|
||||
McpManager::new_with_extensions(
|
||||
Arc::new(PluginsManager::new(config.codex_home.to_path_buf())),
|
||||
Arc::new(plugins_manager_for_config(config)),
|
||||
Arc::new(builder.build()),
|
||||
codex_core::CodexAppsToolsCache::default(),
|
||||
)
|
||||
|
||||
@@ -13,8 +13,6 @@ use codex_protocol::protocol::SkillScope;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_plugins::PluginIdentity;
|
||||
use codex_utils_plugins::PluginSkillRoot;
|
||||
use codex_utils_plugins::SkillDiscoveryMode;
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::sync::Semaphore;
|
||||
use tracing::info;
|
||||
@@ -22,27 +20,23 @@ use tracing::instrument;
|
||||
use tracing::warn;
|
||||
|
||||
use codex_config::SkillsConfig;
|
||||
use codex_core_skills::PluginSkillSnapshots;
|
||||
use codex_core_skills::SkillError;
|
||||
use codex_core_skills::SkillLoadOutcome;
|
||||
use codex_core_skills::config_rules::SkillConfigRules;
|
||||
use codex_core_skills::config_rules::resolve_disabled_skill_paths;
|
||||
use codex_core_skills::config_rules::skill_config_rules_from_stack;
|
||||
use codex_core_skills::loader::MAX_CONCURRENT_ROOT_SCANS;
|
||||
use codex_core_skills::loader::SkillRoot;
|
||||
use codex_core_skills::loader::SkillRootSnapshot;
|
||||
use codex_core_skills::loader::load_skill_root_snapshot;
|
||||
use codex_skills::LoadedSkills;
|
||||
use codex_skills::SkillLoadFuture;
|
||||
use codex_skills::SkillRootLoadRequest;
|
||||
use codex_skills::SkillRootLoader;
|
||||
use codex_skills::SkillRootSnapshots;
|
||||
use codex_skills::install_system_skills;
|
||||
|
||||
use crate::HostSkillsSnapshot;
|
||||
use crate::host_roots::resolve_skill_roots;
|
||||
use crate::loader::HostSkillRoot;
|
||||
use crate::loader::load_and_merge_host_skill_roots;
|
||||
use crate::loader::load_host_skill_root;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HostSkillsLoadInput {
|
||||
@@ -50,7 +44,7 @@ pub struct HostSkillsLoadInput {
|
||||
pub effective_skill_roots: Vec<PluginSkillRoot>,
|
||||
pub config_layer_stack: ConfigLayerStack,
|
||||
pub bundled_skills_enabled: bool,
|
||||
plugin_skill_snapshots: Option<PluginSkillSnapshots>,
|
||||
plugin_skill_snapshots: Option<SkillRootSnapshots<PluginSkillRoot>>,
|
||||
}
|
||||
|
||||
impl HostSkillsLoadInput {
|
||||
@@ -72,7 +66,7 @@ impl HostSkillsLoadInput {
|
||||
/// Attaches plugin skill snapshots parsed during plugin loading, when available.
|
||||
pub fn with_plugin_skill_snapshots(
|
||||
mut self,
|
||||
plugin_skill_snapshots: Option<PluginSkillSnapshots>,
|
||||
plugin_skill_snapshots: Option<SkillRootSnapshots<PluginSkillRoot>>,
|
||||
) -> Self {
|
||||
self.plugin_skill_snapshots = plugin_skill_snapshots;
|
||||
self
|
||||
@@ -294,58 +288,37 @@ impl HostSkillsService {
|
||||
roots: Vec<SkillRoot>,
|
||||
skill_config_rules: &SkillConfigRules,
|
||||
) -> SkillLoadOutcome {
|
||||
let plugin_skill_snapshots = input.plugin_skill_snapshots.as_ref();
|
||||
let mut indexed_snapshots = futures::stream::iter(roots.into_iter().enumerate())
|
||||
.map(|(root_index, root)| async move {
|
||||
let _root_scan_slot = self
|
||||
.root_scan_slots
|
||||
.acquire()
|
||||
.await
|
||||
.unwrap_or_else(|_| unreachable!());
|
||||
let use_legacy_loader = root.plugin_identity.is_some()
|
||||
|| root.plugin_namespace.is_some()
|
||||
|| root.plugin_root.is_some()
|
||||
|| root.discovery_mode != SkillDiscoveryMode::Recursive;
|
||||
let snapshot = if use_legacy_loader {
|
||||
load_skill_root_snapshot(root, plugin_skill_snapshots).await
|
||||
} else {
|
||||
let snapshot = load_host_skill_root(HostSkillRoot::host(
|
||||
root.path,
|
||||
root.scope,
|
||||
root.file_system,
|
||||
))
|
||||
.await;
|
||||
SkillRootSnapshot::new(
|
||||
snapshot.root,
|
||||
snapshot.skills,
|
||||
snapshot.skill_discovery_path_by_path,
|
||||
snapshot
|
||||
.errors
|
||||
.into_iter()
|
||||
.map(|error| SkillError {
|
||||
path: error.path,
|
||||
message: error.message,
|
||||
})
|
||||
.collect(),
|
||||
snapshot.file_system,
|
||||
)
|
||||
};
|
||||
(root_index, snapshot)
|
||||
let roots = roots
|
||||
.into_iter()
|
||||
.map(|root| {
|
||||
match (
|
||||
root.plugin_identity,
|
||||
root.plugin_namespace,
|
||||
root.plugin_root,
|
||||
) {
|
||||
(Some(plugin_identity), Some(plugin_namespace), Some(plugin_root)) => {
|
||||
HostSkillRoot::plugin(
|
||||
PluginSkillRoot {
|
||||
path: root.path,
|
||||
plugin_identity,
|
||||
plugin_namespace,
|
||||
plugin_root,
|
||||
discovery_mode: root.discovery_mode,
|
||||
},
|
||||
root.file_system,
|
||||
)
|
||||
}
|
||||
_ => HostSkillRoot::host(root.path, root.scope, root.file_system),
|
||||
}
|
||||
})
|
||||
.buffer_unordered(MAX_CONCURRENT_ROOT_SCANS)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
indexed_snapshots.sort_unstable_by_key(|(root_index, _)| *root_index);
|
||||
let outcome = SkillLoadOutcome::from_root_snapshots(
|
||||
indexed_snapshots
|
||||
.into_iter()
|
||||
.map(|(_, snapshot)| snapshot)
|
||||
.collect(),
|
||||
);
|
||||
let outcome = codex_core_skills::filter_skill_load_outcome_for_product(
|
||||
outcome,
|
||||
.collect();
|
||||
let outcome = load_and_merge_host_skill_roots(
|
||||
roots,
|
||||
&self.root_scan_slots,
|
||||
self.restriction_product,
|
||||
);
|
||||
input.plugin_skill_snapshots.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let disabled_paths = resolve_disabled_skill_paths(&outcome.skills, skill_config_rules);
|
||||
outcome.with_disabled_paths(disabled_paths)
|
||||
}
|
||||
@@ -441,7 +414,7 @@ impl SkillRootLoader<PluginSkillRoot> for HostSkillsService {
|
||||
struct ConfigSkillsCacheKey {
|
||||
roots: Vec<ConfigSkillRootCacheKey>,
|
||||
skill_config_rules: SkillConfigRules,
|
||||
plugin_skill_snapshots: Option<PluginSkillSnapshots>,
|
||||
plugin_skill_snapshots: Option<SkillRootSnapshots<PluginSkillRoot>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -495,7 +468,7 @@ pub fn bundled_skills_enabled_from_stack(
|
||||
fn config_skills_cache_key(
|
||||
roots: &[SkillRoot],
|
||||
skill_config_rules: &SkillConfigRules,
|
||||
plugin_skill_snapshots: Option<&PluginSkillSnapshots>,
|
||||
plugin_skill_snapshots: Option<&SkillRootSnapshots<PluginSkillRoot>>,
|
||||
) -> ConfigSkillsCacheKey {
|
||||
ConfigSkillsCacheKey {
|
||||
roots: roots
|
||||
|
||||
@@ -5,6 +5,9 @@ use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_skills::LoadedSkillRoot;
|
||||
use codex_skills::SkillRootSnapshotCache;
|
||||
use codex_skills::SkillRootSnapshots;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
@@ -12,13 +15,34 @@ use codex_utils_plugins::PluginIdentity;
|
||||
use codex_utils_plugins::PluginSkillRoot;
|
||||
use codex_utils_plugins::SkillDiscoveryMode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestPluginSkillSnapshotCache {
|
||||
snapshots: Mutex<HashMap<PluginSkillRoot, LoadedSkillRoot>>,
|
||||
}
|
||||
|
||||
impl SkillRootSnapshotCache<PluginSkillRoot> for TestPluginSkillSnapshotCache {
|
||||
fn get(&self, root: &PluginSkillRoot) -> Option<LoadedSkillRoot> {
|
||||
self.snapshots.lock().unwrap().get(root).cloned()
|
||||
}
|
||||
|
||||
fn insert(&self, root: PluginSkillRoot, snapshot: LoadedSkillRoot) {
|
||||
self.snapshots.lock().unwrap().insert(root, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_plugin_skill_snapshots() -> SkillRootSnapshots<PluginSkillRoot> {
|
||||
SkillRootSnapshots::new(Arc::new(TestPluginSkillSnapshotCache::default()))
|
||||
}
|
||||
|
||||
fn write_user_skill(codex_home: &TempDir, dir: &str, name: &str, description: &str) {
|
||||
let skill_dir = codex_home.path().join("skills").join(dir);
|
||||
fs::create_dir_all(&skill_dir).unwrap();
|
||||
@@ -307,7 +331,7 @@ async fn skills_for_config_refreshes_cache_when_remote_plugin_id_changes() {
|
||||
config_layer_stack.clone(),
|
||||
bundled_skills_enabled_from_stack(&config_layer_stack),
|
||||
)
|
||||
.with_plugin_skill_snapshots(Some(PluginSkillSnapshots::for_plugin_load()));
|
||||
.with_plugin_skill_snapshots(Some(test_plugin_skill_snapshots()));
|
||||
let plugin_snapshot = skills_service
|
||||
.snapshot_for_config(&plugin_input, Some(Arc::clone(&LOCAL_FS)))
|
||||
.await;
|
||||
@@ -717,7 +741,7 @@ async fn skills_for_cwd_uses_cached_result_until_force_reload() {
|
||||
);
|
||||
let config_input = base_input
|
||||
.clone()
|
||||
.with_plugin_skill_snapshots(Some(PluginSkillSnapshots::for_plugin_load()));
|
||||
.with_plugin_skill_snapshots(Some(test_plugin_skill_snapshots()));
|
||||
let (config_snapshot, snapshot_a) = tokio::join!(
|
||||
skills_service.snapshot_for_config(&config_input, Some(Arc::clone(&LOCAL_FS))),
|
||||
skills_service.snapshot_for_cwd(
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::utils::invalid_data_error;
|
||||
use crate::utils::is_missing_or_empty_text_file;
|
||||
use codex_config::types::PluginConfig;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_core::plugins_manager_for_config;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
@@ -351,7 +351,7 @@ impl ExternalAgentConfigService {
|
||||
.unwrap_or_default();
|
||||
let configured_marketplace_plugins = configured_marketplace_plugins(
|
||||
&config,
|
||||
&PluginsManager::new(self.codex_home.clone()),
|
||||
&plugins_manager_for_config(&config),
|
||||
)?;
|
||||
let source_root = repo_root.unwrap_or(self.external_agent_home.as_path());
|
||||
if let Some(detected) =
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use codex_analytics::PluginInstallSource;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::plugins_manager_for_config;
|
||||
use codex_core_plugins::PluginInstallError;
|
||||
use codex_core_plugins::PluginInstallRequest;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_core_plugins::marketplace::MarketplaceError;
|
||||
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
|
||||
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
|
||||
@@ -101,7 +101,7 @@ impl ExternalAgentConfigService {
|
||||
.map_err(|err| io::Error::other(format!("failed to load config: {err}")))?;
|
||||
let requirements = config.config_layer_stack.requirements().clone();
|
||||
let mut outcome = PluginImportOutcome::default();
|
||||
let plugins_manager = PluginsManager::new(self.codex_home.clone())
|
||||
let plugins_manager = plugins_manager_for_config(&config)
|
||||
.with_plugin_install_source(PluginInstallSource::ExternalAgentMigration);
|
||||
if let Some(analytics_events_client) = self.analytics_events_client.clone() {
|
||||
plugins_manager.set_analytics_events_client(analytics_events_client);
|
||||
|
||||
Reference in New Issue
Block a user