ext/skills/src/provider/host.rs
134 lines
use codex_core_skills::SkillLoadOutcome;use codex_core_skills::SkillMetadata;use crate::catalog::SkillAuthority;use crate::catalog::SkillCatalog;use crate::catalog::SkillCatalogEntry;use crate::catalog::SkillPackageId;use crate::catalog::SkillProviderError;use crate::catalog::SkillReadResult;use crate::catalog::SkillResourceId;use crate::catalog::SkillSearchResult;use crate::catalog::SkillSourceKind;use crate::provider::SkillListQuery;use crate::provider::SkillProvider;use crate::provider::SkillProviderFuture;use crate::provider::SkillReadRequest;use crate::provider::SkillSearchRequest;const HOST_AUTHORITY_ID: &str = "host";/// Host-owned skill provider backed by the already-loaded turn skills.////// The provider intentionally does not reload or cache host skills. Core owns/// skill loading, including plugin roots, runtime extra roots, and the primary/// environment filesystem. This adapter only maps that loaded outcome into the/// skills-extension catalog/read contract.#[derive(Clone, Default)]pub struct HostSkillProvider;impl HostSkillProvider { pub fn new() -> Self { Self }}impl SkillProvider for HostSkillProvider { fn list(&self, query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog> { Box::pin(async move { let Some(host_loaded_skills) = query.host else { return Err(SkillProviderError::new( "host skill provider requires loaded host skills", )); }; Ok(catalog_from_outcome(host_loaded_skills.outcome())) }) } fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult> { Box::pin(async move { let Some(host_loaded_skills) = request.host else { return Err(SkillProviderError::new( "host skill provider requires loaded host skills", )); }; let Some(skill) = host_loaded_skills.outcome().skills.iter().find(|skill| { let skill_path = skill.path_to_skills_md.to_string_lossy(); skill_path == request.resource.as_str() || skill_path.replace('\\', "/") == request.resource.as_str() }) else { return Err(SkillProviderError::new(format!( "host skill resource is not loaded: {}", request.resource.as_str() ))); }; let contents = host_loaded_skills .read_skill_text(skill) .await .map_err(|err| { SkillProviderError::new(format!( "failed to read host skill resource {}: {err}", request.resource.as_str() )) })?; Ok(SkillReadResult { resource: request.resource, contents, }) }) } fn search(&self, _request: SkillSearchRequest) -> SkillProviderFuture<'_, SkillSearchResult> { Box::pin(async { Ok(SkillSearchResult::default()) }) }}fn catalog_from_outcome(outcome: &SkillLoadOutcome) -> SkillCatalog { let mut catalog = SkillCatalog { entries: Vec::new(), warnings: outcome .errors .iter() .map(|err| { format!( "Failed to load skill at {}: {}", err.path.display(), err.message ) }) .collect(), }; for (skill, enabled) in outcome.skills_with_enabled() { catalog.push_entry(catalog_entry_from_skill(skill, enabled)); } catalog}fn catalog_entry_from_skill(skill: &SkillMetadata, enabled: bool) -> SkillCatalogEntry { let skill_path = skill.path_to_skills_md.to_string_lossy().into_owned(); let display_path = skill_path.replace('\\', "/"); let mut entry = SkillCatalogEntry::new( SkillPackageId(skill_path.clone()), SkillAuthority::new(SkillSourceKind::Host, HOST_AUTHORITY_ID), skill.name.clone(), skill.description.clone(), SkillResourceId::new(skill_path), ) .with_short_description(skill.short_description.clone()) .with_display_path(display_path) .with_dependencies(skill.dependencies.clone()); if !enabled { entry = entry.disabled(); } if !skill.allows_implicit_invocation() { entry = entry.hidden_from_prompt(); } entry}