mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user