mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Unify plugin skill loading with the host skill service (#37444)
## What changed - Inject the host skill loader into `PluginsManager` so plugin discovery and agent turns use the same loading and product-policy behavior. - Share plugin skill snapshots across those paths, preserving a consistent view of skills across workspaces. - Apply migrated-command precedence after product filtering, allowing an eligible migrated command to replace a filtered native skill with the same name. ## Testing - Add coverage for product-restricted plugin skills, native-versus-migrated command precedence, and the skills exposed to agent turns. GitOrigin-RevId: f5ef0d0766ebeeb30d73ffaf044d003c2906ea4d
This commit is contained in:
@@ -1,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