use crate::core::blob_storage::BlobStorageConfig;
use crate::events::EventBus;
use crate::execution::{ExecutionConfig, ExecutionSandbox};
use crate::storage::ZipHandler;
use crate::validation::{SkillValidator, ZipValidator};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::info;
#[derive(Debug, Clone, Default)]
pub struct HttpServerConfig {
pub allowed_origins: Vec<String>,
pub allowed_headers: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ServiceConfig {
pub skill_storage_path: PathBuf,
pub execution: ExecutionConfig,
pub hot_reload: HotReloadConfig,
pub cache: CacheConfig,
pub embedding: Option<EmbeddingConfig>,
pub security: SecurityConfig,
pub staging_dir: Option<PathBuf>,
pub registry_blob_storage: Option<BlobStorageConfig>,
pub registry_index_path: Option<PathBuf>,
pub registry_blob_base_url: Option<String>,
pub http_server: Option<HttpServerConfig>,
}
impl Default for ServiceConfig {
fn default() -> Self {
Self {
skill_storage_path: PathBuf::from("./skills"),
execution: ExecutionConfig::default(),
hot_reload: HotReloadConfig::default(),
cache: CacheConfig::default(),
embedding: None,
security: SecurityConfig::default(),
staging_dir: None,
registry_blob_storage: None,
registry_index_path: None,
registry_blob_base_url: None,
http_server: None,
}
}
}
#[derive(Debug, Clone)]
pub struct HotReloadConfig {
pub enabled: bool,
pub watch_paths: Vec<PathBuf>,
pub debounce_ms: u64,
pub auto_reload: bool,
}
impl Default for HotReloadConfig {
fn default() -> Self {
Self {
enabled: true,
watch_paths: vec![PathBuf::from("./skills")],
debounce_ms: 1000,
auto_reload: true,
}
}
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_size: usize,
pub metadata_ttl: u64,
pub content_ttl: u64,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_size: 1000,
metadata_ttl: 300, content_ttl: 60, }
}
}
#[derive(Debug, Clone)]
pub struct EmbeddingConfig {
pub openai_base_url: String,
pub embedding_model: String,
pub index_path: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct SecurityConfig {
pub enable_sandbox: bool,
pub allowed_paths: Vec<PathBuf>,
pub audit_logging: bool,
pub max_execution_time: std::time::Duration,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
enable_sandbox: true,
allowed_paths: vec![PathBuf::from("/tmp"), PathBuf::from("./temp")],
audit_logging: true,
max_execution_time: std::time::Duration::from_secs(60),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SkillId(String);
impl SkillId {
pub fn new(id: String) -> Result<Self, ServiceError> {
if id.trim().is_empty() {
return Err(ServiceError::Validation(
"Skill ID cannot be empty".to_string(),
));
}
if id.len() > 255 {
return Err(ServiceError::Validation(
"Skill ID too long (max 255 characters)".to_string(),
));
}
if id.contains('/') {
return Err(ServiceError::Validation(
"Skill ID cannot contain forward slashes. Scope should be handled separately during publishing.".to_string(),
));
}
if !id
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(ServiceError::Validation("Skill ID contains invalid characters (only alphanumeric, dash, underscore allowed)".to_string()));
}
Ok(Self(id))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::fmt::Display for SkillId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<SkillId> for String {
fn from(id: SkillId) -> String {
id.0
}
}
impl TryFrom<String> for SkillId {
type Error = ServiceError;
fn try_from(s: String) -> Result<Self, Self::Error> {
SkillId::new(s)
}
}
impl AsRef<str> for SkillId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl serde::Serialize for SkillId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for SkillId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
SkillId::new(s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ServiceError {
#[error("Storage error: {0}")]
Storage(String),
#[error("Execution error: {0}")]
Execution(#[from] crate::execution::ExecutionError),
#[error("Validation error: {0}")]
Validation(String),
#[error("Event error: {0}")]
Event(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Configuration error: {0}")]
Config(String),
#[error("Skill not found: {0}")]
SkillNotFound(String),
#[error("Invalid operation: {0}")]
InvalidOperation(String),
#[error("Custom error: {0}")]
Custom(String),
}
pub struct FastSkillService {
config: ServiceConfig,
skill_manager: Arc<dyn crate::core::skill_manager::SkillManagementService>,
metadata_service: Arc<dyn crate::core::metadata::MetadataService>,
vector_index_service: Option<Arc<dyn crate::core::vector_index::VectorIndexService>>,
storage: Arc<dyn crate::storage::StorageBackend>,
#[allow(dead_code)]
zip_handler: Arc<ZipHandler>,
#[allow(dead_code)]
sandbox: Arc<ExecutionSandbox>,
#[allow(dead_code)]
skill_validator: Arc<SkillValidator>,
#[allow(dead_code)]
zip_validator: Arc<ZipValidator>,
#[allow(dead_code)]
event_bus: Arc<EventBus>,
hot_reload_manager: Option<Arc<crate::storage::hot_reload::HotReloadManager>>,
initialized: bool,
}
impl FastSkillService {
pub async fn new(config: ServiceConfig) -> Result<Self, ServiceError> {
crate::init_logging();
info!("Initializing FastSkill service v{}", crate::VERSION);
let storage = Arc::new(
crate::storage::FilesystemStorage::new(config.skill_storage_path.clone()).await?,
);
let zip_handler = Arc::new(crate::storage::ZipHandler::new()?);
let sandbox = Arc::new(crate::execution::ExecutionSandbox::new(
config.execution.clone(),
)?);
let skill_validator = Arc::new(crate::validation::SkillValidator::new());
let zip_validator = Arc::new(crate::validation::ZipValidator::new());
let event_bus = Arc::new(crate::events::EventBus::new());
let skill_manager = Arc::new(crate::core::skill_manager::SkillManager::new());
let metadata_service = Arc::new(crate::core::metadata::MetadataServiceImpl::new(
skill_manager.clone(),
));
let vector_index_service = if let Some(embedding_config) = &config.embedding {
Some(Arc::new(
crate::core::vector_index::VectorIndexServiceImpl::with_config(
embedding_config,
&config.skill_storage_path,
),
)
as Arc<dyn crate::core::vector_index::VectorIndexService>)
} else {
None
};
let hot_reload_manager = if config.hot_reload.enabled {
Some(Arc::new(crate::storage::hot_reload::HotReloadManager::new(
storage.clone(),
event_bus.clone(),
)?))
} else {
None
};
Ok(Self {
config,
skill_manager,
metadata_service,
vector_index_service,
storage,
zip_handler,
sandbox,
skill_validator,
zip_validator,
event_bus,
hot_reload_manager,
initialized: false,
})
}
pub async fn initialize(&mut self) -> Result<(), ServiceError> {
if self.initialized {
return Ok(());
}
info!("Initializing service components...");
self.storage.initialize().await?;
if let Some(hot_reload) = &self.hot_reload_manager {
hot_reload
.enable_hot_reloading(self.config.hot_reload.watch_paths.clone())
.await?;
}
self.auto_index_skills_from_filesystem().await?;
self.initialized = true;
info!("Service initialization complete");
Ok(())
}
pub async fn shutdown(&mut self) -> Result<(), ServiceError> {
info!("Shutting down service...");
if let Some(hot_reload) = &self.hot_reload_manager {
hot_reload.disable_hot_reloading().await?;
}
self.storage.clear_cache().await?;
self.initialized = false;
info!("Service shutdown complete");
Ok(())
}
pub fn skill_manager(&self) -> Arc<dyn crate::core::skill_manager::SkillManagementService> {
self.skill_manager.clone()
}
pub fn metadata_service(&self) -> Arc<dyn crate::core::metadata::MetadataService> {
self.metadata_service.clone()
}
pub fn vector_index_service(
&self,
) -> Option<Arc<dyn crate::core::vector_index::VectorIndexService>> {
self.vector_index_service.clone()
}
pub fn loading_service(&self) -> Arc<dyn crate::core::loading::ProgressiveLoadingService> {
Arc::new(crate::core::loading::LoadingService::new())
}
pub fn tool_service(&self) -> Arc<dyn crate::core::tool_calling::ToolCallingService> {
Arc::new(crate::core::tool_calling::ToolCallingServiceImpl::new())
}
pub fn routing_service(&self) -> Arc<dyn crate::core::routing::RoutingService> {
Arc::new(crate::core::routing::RoutingServiceImpl::new(
self.metadata_service.clone(),
))
}
pub fn config(&self) -> &ServiceConfig {
&self.config
}
pub fn context_resolver(&self) -> crate::core::context_resolver::ContextResolver {
crate::core::context_resolver::ContextResolver::new(
self.skill_manager.clone(),
self.metadata_service.clone(),
self.vector_index_service.clone(),
self.config.embedding.clone(),
self.config.skill_storage_path.clone(),
)
}
pub fn is_initialized(&self) -> bool {
self.initialized
}
async fn auto_index_skills_from_filesystem(&self) -> Result<(), ServiceError> {
use walkdir::WalkDir;
let mut indexed_count = 0;
for entry in WalkDir::new(&self.config.skill_storage_path)
.into_iter()
.filter_entry(|e| {
!e.file_name()
.to_str()
.map(|s| {
s.starts_with('.')
|| s == "node_modules"
|| s == "target"
|| s == "__pycache__"
})
.unwrap_or(false)
})
{
let entry = entry.map_err(|e| {
ServiceError::Custom(format!("Failed to read directory entry: {}", e))
})?;
if entry.file_type().is_file() {
let fname = entry.file_name();
if fname == "SKILL.md" || fname == "skill.md" {
let skill_file = entry.path();
match self.try_index_skill_from_file(skill_file).await {
Ok(_) => {
indexed_count += 1;
}
Err(e) => {
tracing::warn!(
"Failed to index skill at {}: {}",
skill_file.display(),
e
);
}
}
}
}
}
if indexed_count > 0 {
info!("Auto-indexed {} skills from filesystem", indexed_count);
}
Ok(())
}
async fn try_index_skill_from_file(
&self,
skill_file: &std::path::Path,
) -> Result<(), ServiceError> {
let content = tokio::fs::read_to_string(skill_file)
.await
.map_err(|e| ServiceError::Custom(format!("Failed to read SKILL.md: {}", e)))?;
let frontmatter = crate::core::metadata::parse_yaml_frontmatter(&content)?;
let skill_dir = skill_file
.parent()
.ok_or_else(|| ServiceError::Custom("SKILL.md has no parent directory".to_string()))?;
let skill_id_str = skill_dir
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| ServiceError::Custom("Invalid skill directory name".to_string()))?
.to_string();
let skill_id = SkillId::new(skill_id_str)?;
let mut skill = crate::core::skill_manager::SkillDefinition::new(
skill_id.clone(),
frontmatter.name,
frontmatter.description,
frontmatter.version.unwrap_or_else(|| "1.0.0".to_string()),
);
skill.author = frontmatter.author;
skill.skill_file = skill_file.to_path_buf();
skill.created_at = chrono::Utc::now();
skill.updated_at = chrono::Utc::now();
match self.skill_manager.register_skill(skill).await {
Ok(_) => Ok(()),
Err(crate::core::service::ServiceError::Custom(msg))
if msg.contains("already exists") =>
{
Ok(())
}
Err(e) => Err(e),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_service_creation() {
let temp_dir = TempDir::new().unwrap();
let config = ServiceConfig {
skill_storage_path: temp_dir.path().to_path_buf(),
..Default::default()
};
let mut service = FastSkillService::new(config).await.unwrap();
assert!(!service.is_initialized());
service.initialize().await.unwrap();
assert!(service.is_initialized());
}
#[tokio::test]
async fn test_service_shutdown() {
let temp_dir = TempDir::new().unwrap();
let config = ServiceConfig {
skill_storage_path: temp_dir.path().to_path_buf(),
..Default::default()
};
let mut service = FastSkillService::new(config).await.unwrap();
service.initialize().await.unwrap();
service.shutdown().await.unwrap();
assert!(!service.is_initialized());
}
#[test]
fn test_skill_id_new_validates_input() {
assert!(SkillId::new("valid-id".to_string()).is_ok());
assert!(SkillId::new("valid_id_123".to_string()).is_ok());
assert!(SkillId::new("".to_string()).is_err());
assert!(SkillId::new("bad/id".to_string()).is_err());
assert!(SkillId::new("id with spaces".to_string()).is_err());
}
#[test]
fn test_skill_id_try_from_validates_input() {
assert!(SkillId::try_from("valid-id".to_string()).is_ok());
assert!(SkillId::try_from("".to_string()).is_err());
assert!(SkillId::try_from("bad/id".to_string()).is_err());
}
}