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

## What changed

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

## Testing

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

GitOrigin-RevId: f5ef0d0766ebeeb30d73ffaf044d003c2906ea4d
This commit is contained in:
felixxia-oai
2026-08-07 13:54:24 +00:00
committed by copyberry
parent e75a1888d7
commit e58d9ef447
30 changed files with 682 additions and 272 deletions
+20 -19
View File
@@ -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 {
+1
View File
@@ -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)]
+53 -12
View File
@@ -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,
+3 -3
View File
@@ -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(
+22 -9
View File
@@ -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,
+81 -79
View File
@@ -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()))
}
+185
View File
@@ -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)
}