vtcode_core/skills/loader/
enhanced.rs1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4
5use super::SkillLoaderConfig;
6use super::discovery::{find_git_root, skill_roots_with_home_dir};
7use crate::skills::cli_bridge::CliToolBridge;
8use crate::skills::command_skills::{
9 BuiltInCommandSkill, CommandSkillBackend, built_in_command_skill, find_command_skill_by_skill_name,
10 merge_built_in_command_skill_contexts,
11};
12use crate::skills::container_validation::{
13 ContainerSkillsValidator, ContainerValidationReport, ContainerValidationResult,
14};
15use crate::skills::discovery::{DiscoveryConfig, DiscoveryResult, SkillDiscovery};
16use crate::skills::model::SkillScope;
17use crate::skills::system::{install_system_skills, system_cache_root_dir};
18use crate::skills::types::{Skill, SkillContext};
19use crate::tools::error_messages::skill_ops;
20
21pub enum EnhancedSkill {
23 Traditional(Box<Skill>),
25 CliTool(Box<CliToolBridge>),
27 BuiltInCommand(Box<BuiltInCommandSkill>),
29 NativePlugin(Box<dyn crate::skills::native_plugin::NativePluginTrait>),
31}
32
33impl std::fmt::Debug for EnhancedSkill {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 Self::Traditional(skill) => f.debug_tuple("Traditional").field(skill).finish(),
37 Self::CliTool(tool) => f.debug_tuple("CliTool").field(tool).finish(),
38 Self::BuiltInCommand(skill) => f.debug_tuple("BuiltInCommand").field(skill).finish(),
39 Self::NativePlugin(plugin) => f.debug_tuple("NativePlugin").field(plugin).finish(),
40 }
41 }
42}
43
44pub struct EnhancedSkillLoader {
46 workspace_root: PathBuf,
47 codex_home: PathBuf,
48 discovery: SkillDiscovery,
49}
50
51fn default_codex_home() -> PathBuf {
52 std::env::var_os("CODEX_HOME")
53 .filter(|value| !value.is_empty())
54 .map(PathBuf::from)
55 .or_else(|| dirs::home_dir().map(|home| home.join(".codex")))
56 .unwrap_or_else(|| PathBuf::from(".codex"))
57}
58
59pub(crate) fn discovery_config_for_codex_home(workspace_root: &Path, codex_home: &Path) -> DiscoveryConfig {
60 let home_dir = dirs::home_dir();
61 let loader_config = SkillLoaderConfig {
62 codex_home: codex_home.to_path_buf(),
63 cwd: workspace_root.to_path_buf(),
64 project_root: find_git_root(workspace_root),
65 include_bundled_system_skills: true,
66 };
67 let roots = skill_roots_with_home_dir(&loader_config, home_dir.as_deref());
68
69 DiscoveryConfig {
70 skill_paths: roots
71 .iter()
72 .filter(|root| !root.is_tool_root && !root.is_plugin_root)
73 .map(|root| root.path.clone())
74 .collect(),
75 tool_paths: roots
76 .iter()
77 .filter(|root| root.is_tool_root)
78 .map(|root| root.path.clone())
79 .collect(),
80 ..Default::default()
81 }
82}
83
84impl EnhancedSkillLoader {
85 pub fn new(workspace_root: PathBuf) -> Self {
87 let codex_home = default_codex_home();
88 let discovery = SkillDiscovery::with_config(discovery_config_for_codex_home(&workspace_root, &codex_home));
89 Self { workspace_root, codex_home, discovery }
90 }
91
92 pub fn with_codex_home(workspace_root: PathBuf, codex_home: PathBuf) -> Self {
94 let discovery = SkillDiscovery::with_config(discovery_config_for_codex_home(&workspace_root, &codex_home));
95 Self { workspace_root, codex_home, discovery }
96 }
97
98 fn ensure_system_skills_installed(&self) {
99 if let Err(err) = install_system_skills(&self.codex_home) {
100 tracing::warn!("enhanced skill loader failed to install bundled system skills: {err}");
101 }
102 }
103
104 pub async fn discover_all_skills(&mut self) -> Result<DiscoveryResult> {
106 self.ensure_system_skills_installed();
107 let mut result = self.discovery.discover_all(&self.workspace_root).await?;
108 merge_built_in_command_skill_contexts(&mut result.skills);
109 Ok(result)
110 }
111
112 pub async fn get_skill(&mut self, name: &str) -> Result<EnhancedSkill> {
114 self.ensure_system_skills_installed();
115 let result = self.discovery.discover_all(&self.workspace_root).await?;
116
117 if let Some(skill_ctx) = select_traditional_skill_ctx(&result.skills, name, &self.workspace_root) {
121 let path = skill_ctx.path();
122 let (manifest, instructions) = crate::skills::manifest::parse_skill_file(path)?;
123 let skill = Skill::with_scope(
124 manifest,
125 path.clone(),
126 infer_scope_from_skill_path(path, &self.workspace_root),
127 instructions,
128 )?;
129 return Ok(EnhancedSkill::Traditional(Box::new(skill)));
130 }
131
132 for tool_config in &result.tools {
134 if tool_config.name == name {
135 let bridge = CliToolBridge::new(tool_config.clone())?;
136 return Ok(EnhancedSkill::CliTool(Box::new(bridge)));
137 }
138 }
139
140 if let Some(skill) = built_in_command_skill(name) {
141 return Ok(EnhancedSkill::BuiltInCommand(Box::new(skill)));
142 }
143
144 Err(skill_ops::skill_not_found_error(name))
149 }
150
151 pub async fn generate_validation_report(&mut self) -> Result<ContainerValidationReport> {
153 let result = self.discovery.discover_all(&self.workspace_root).await?;
154 let mut report = ContainerValidationReport::new();
155 let validator = ContainerSkillsValidator::new();
156
157 for skill_ctx in &result.skills {
158 match self.load_full_skill_from_ctx(skill_ctx) {
159 Ok(skill) => {
160 let analysis = validator.analyze_skill(&skill);
161 report.add_skill_analysis(skill.name().to_string(), analysis);
162 }
163 Err(e) => {
164 report.add_incompatible_skill(
165 skill_ctx.manifest().name.clone(),
166 skill_ctx.manifest().description.clone(),
167 format!("Load error: {e}"),
168 );
169 }
170 }
171 }
172
173 report.finalize();
174 Ok(report)
175 }
176
177 pub fn check_container_requirements(&self, skill: &Skill) -> ContainerValidationResult {
179 let validator = ContainerSkillsValidator::new();
180 validator.analyze_skill(skill)
181 }
182
183 fn load_full_skill_from_ctx(&self, ctx: &SkillContext) -> Result<Skill> {
184 let path = ctx.path();
185 let (manifest, instructions) = crate::skills::manifest::parse_skill_file(path)?;
186 Skill::with_scope(manifest, path.clone(), infer_scope_from_skill_path(path, &self.workspace_root), instructions)
187 }
188}
189
190fn infer_scope_from_skill_path(path: &Path, workspace_root: &Path) -> SkillScope {
191 if path.starts_with(Path::new("/etc/codex/skills")) {
192 return SkillScope::Admin;
193 }
194 if path.starts_with(system_cache_root_dir(&default_codex_home())) {
195 return SkillScope::System;
196 }
197 if let Some(home) = dirs::home_dir()
198 && path.starts_with(home.join(".agents/skills"))
199 {
200 return SkillScope::User;
201 }
202 if path.starts_with(workspace_root) {
203 return SkillScope::Repo;
204 }
205
206 if has_agents_skills_ancestor(path) {
211 return SkillScope::Repo;
212 }
213
214 SkillScope::User
215}
216
217fn has_agents_skills_ancestor(path: &Path) -> bool {
218 let mut ancestors = path.parent().map(Path::new).into_iter().peekable();
219 while let Some(current) = ancestors.next() {
220 let is_skills = current.file_name().and_then(|f| f.to_str()) == Some("skills");
221 let parent_is_agents = ancestors.peek().and_then(|p| p.file_name().and_then(|f| f.to_str())) == Some(".agents");
222 if is_skills && parent_is_agents {
223 return true;
224 }
225 }
226 false
227}
228
229fn is_bundled_command_skill(name: &str) -> bool {
230 find_command_skill_by_skill_name(name)
231 .is_some_and(|spec| matches!(spec.backend, CommandSkillBackend::TraditionalSkill { .. }))
232}
233
234fn is_system_skill_path(path: &Path) -> bool {
235 let mut components = path.components().peekable();
236 while let Some(component) = components.next() {
237 let is_skills = component.as_os_str() == "skills";
238 let next_is_system = components.peek().is_some_and(|next| next.as_os_str() == ".system");
239 if is_skills && next_is_system {
240 return true;
241 }
242 }
243 false
244}
245
246pub(crate) fn select_traditional_skill_ctx<'a>(
247 skills: &'a [SkillContext],
248 name: &str,
249 workspace_root: &Path,
250) -> Option<&'a SkillContext> {
251 let mut first_match: Option<&'a SkillContext> = None;
252 let mut system_match: Option<&'a SkillContext> = None;
253 for skill_ctx in skills {
254 if skill_ctx.manifest().name != name {
255 continue;
256 }
257 if first_match.is_none() {
258 first_match = Some(skill_ctx);
259 }
260 let scope = infer_scope_from_skill_path(skill_ctx.path(), workspace_root);
264 if system_match.is_none()
265 && (scope == SkillScope::System || (scope != SkillScope::Repo && is_system_skill_path(skill_ctx.path())))
266 {
267 system_match = Some(skill_ctx);
268 }
269 if first_match.is_some() && system_match.is_some() {
270 break;
271 }
272 }
273 if is_bundled_command_skill(name) {
274 system_match.or(first_match)
275 } else {
276 first_match
277 }
278}