1use crate::core::blob_storage::BlobStorageConfig;
4use crate::events::EventBus;
5use crate::execution::{ExecutionConfig, ExecutionSandbox};
6use crate::storage::ZipHandler;
7use crate::validation::{SkillValidator, ZipValidator};
8use std::path::PathBuf;
9use std::sync::Arc;
10use tracing::info;
11
12#[derive(Debug, Clone, Default)]
14pub struct HttpServerConfig {
15 pub allowed_origins: Vec<String>,
17
18 pub allowed_headers: Vec<String>,
21}
22
23#[derive(Debug, Clone)]
25pub struct ServiceConfig {
26 pub skill_storage_path: PathBuf,
28
29 pub execution: ExecutionConfig,
31
32 pub hot_reload: HotReloadConfig,
34
35 pub cache: CacheConfig,
37
38 pub embedding: Option<EmbeddingConfig>,
40
41 pub security: SecurityConfig,
43
44 pub staging_dir: Option<PathBuf>,
46
47 pub registry_blob_storage: Option<BlobStorageConfig>,
49
50 pub registry_index_path: Option<PathBuf>,
52
53 pub registry_blob_base_url: Option<String>,
55
56 pub http_server: Option<HttpServerConfig>,
58}
59
60impl Default for ServiceConfig {
61 fn default() -> Self {
62 Self {
63 skill_storage_path: PathBuf::from("./skills"),
64 execution: ExecutionConfig::default(),
65 hot_reload: HotReloadConfig::default(),
66 cache: CacheConfig::default(),
67 embedding: None,
68 security: SecurityConfig::default(),
69 staging_dir: None,
70 registry_blob_storage: None,
71 registry_index_path: None,
72 registry_blob_base_url: None,
73 http_server: None,
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct HotReloadConfig {
81 pub enabled: bool,
83
84 pub watch_paths: Vec<PathBuf>,
86
87 pub debounce_ms: u64,
89
90 pub auto_reload: bool,
92}
93
94impl Default for HotReloadConfig {
95 fn default() -> Self {
96 Self {
97 enabled: true,
98 watch_paths: vec![PathBuf::from("./skills")],
99 debounce_ms: 1000,
100 auto_reload: true,
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct CacheConfig {
108 pub max_size: usize,
110
111 pub metadata_ttl: u64,
113
114 pub content_ttl: u64,
116}
117
118impl Default for CacheConfig {
119 fn default() -> Self {
120 Self {
121 max_size: 1000,
122 metadata_ttl: 300, content_ttl: 60, }
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct EmbeddingConfig {
131 pub openai_base_url: String,
133
134 pub embedding_model: String,
136
137 pub index_path: Option<PathBuf>,
139}
140
141#[derive(Debug, Clone)]
143pub struct SecurityConfig {
144 pub enable_sandbox: bool,
146
147 pub allowed_paths: Vec<PathBuf>,
149
150 pub audit_logging: bool,
152
153 pub max_execution_time: std::time::Duration,
155}
156
157impl Default for SecurityConfig {
158 fn default() -> Self {
159 Self {
160 enable_sandbox: true,
161 allowed_paths: vec![PathBuf::from("/tmp"), PathBuf::from("./temp")],
162 audit_logging: true,
163 max_execution_time: std::time::Duration::from_secs(60),
164 }
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Hash)]
170pub struct SkillId(String);
171
172impl SkillId {
173 pub fn new(id: String) -> Result<Self, ServiceError> {
175 if id.trim().is_empty() {
176 return Err(ServiceError::Validation(
177 "Skill ID cannot be empty".to_string(),
178 ));
179 }
180 if id.len() > 255 {
181 return Err(ServiceError::Validation(
182 "Skill ID too long (max 255 characters)".to_string(),
183 ));
184 }
185 if id.contains('/') {
187 return Err(ServiceError::Validation(
188 "Skill ID cannot contain forward slashes. Scope should be handled separately during publishing.".to_string(),
189 ));
190 }
191 if !id
193 .chars()
194 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
195 {
196 return Err(ServiceError::Validation("Skill ID contains invalid characters (only alphanumeric, dash, underscore allowed)".to_string()));
197 }
198 Ok(Self(id))
199 }
200
201 pub fn as_str(&self) -> &str {
203 &self.0
204 }
205
206 pub fn into_string(self) -> String {
208 self.0
209 }
210}
211
212impl std::fmt::Display for SkillId {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 write!(f, "{}", self.0)
215 }
216}
217
218impl From<SkillId> for String {
219 fn from(id: SkillId) -> String {
220 id.0
221 }
222}
223
224impl TryFrom<String> for SkillId {
225 type Error = ServiceError;
226
227 fn try_from(s: String) -> Result<Self, Self::Error> {
228 SkillId::new(s)
229 }
230}
231
232impl AsRef<str> for SkillId {
233 fn as_ref(&self) -> &str {
234 &self.0
235 }
236}
237
238impl serde::Serialize for SkillId {
239 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
240 where
241 S: serde::Serializer,
242 {
243 serializer.serialize_str(&self.0)
244 }
245}
246
247impl<'de> serde::Deserialize<'de> for SkillId {
248 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
249 where
250 D: serde::Deserializer<'de>,
251 {
252 let s = String::deserialize(deserializer)?;
253 SkillId::new(s).map_err(serde::de::Error::custom)
254 }
255}
256
257#[derive(Debug, thiserror::Error)]
259pub enum ServiceError {
260 #[error("Storage error: {0}")]
261 Storage(String),
262
263 #[error("Execution error: {0}")]
264 Execution(#[from] crate::execution::ExecutionError),
265
266 #[error("Validation error: {0}")]
267 Validation(String),
268
269 #[error("Event error: {0}")]
270 Event(String),
271
272 #[error("IO error: {0}")]
273 Io(#[from] std::io::Error),
274
275 #[error("Configuration error: {0}")]
276 Config(String),
277
278 #[error("Skill not found: {0}")]
279 SkillNotFound(String),
280
281 #[error("Invalid operation: {0}")]
282 InvalidOperation(String),
283
284 #[error("Custom error: {0}")]
285 Custom(String),
286}
287
288pub struct FastSkillService {
293 config: ServiceConfig,
295
296 skill_manager: Arc<dyn crate::core::skill_manager::SkillManagementService>,
298
299 metadata_service: Arc<dyn crate::core::metadata::MetadataService>,
301
302 vector_index_service: Option<Arc<dyn crate::core::vector_index::VectorIndexService>>,
304
305 storage: Arc<dyn crate::storage::StorageBackend>,
307
308 #[allow(dead_code)]
310 zip_handler: Arc<ZipHandler>,
311
312 #[allow(dead_code)]
314 sandbox: Arc<ExecutionSandbox>,
315
316 #[allow(dead_code)]
318 skill_validator: Arc<SkillValidator>,
319
320 #[allow(dead_code)]
322 zip_validator: Arc<ZipValidator>,
323
324 #[allow(dead_code)]
326 event_bus: Arc<EventBus>,
327
328 hot_reload_manager: Option<Arc<crate::storage::hot_reload::HotReloadManager>>,
330
331 initialized: bool,
333}
334
335impl FastSkillService {
336 pub async fn new(config: ServiceConfig) -> Result<Self, ServiceError> {
338 crate::init_logging();
340
341 info!("Initializing FastSkill service v{}", crate::VERSION);
342
343 let storage = Arc::new(
345 crate::storage::FilesystemStorage::new(config.skill_storage_path.clone()).await?,
346 );
347
348 let zip_handler = Arc::new(crate::storage::ZipHandler::new()?);
350
351 let sandbox = Arc::new(crate::execution::ExecutionSandbox::new(
353 config.execution.clone(),
354 )?);
355
356 let skill_validator = Arc::new(crate::validation::SkillValidator::new());
358 let zip_validator = Arc::new(crate::validation::ZipValidator::new());
359
360 let event_bus = Arc::new(crate::events::EventBus::new());
362
363 let skill_manager = Arc::new(crate::core::skill_manager::SkillManager::new());
365
366 let metadata_service = Arc::new(crate::core::metadata::MetadataServiceImpl::new(
368 skill_manager.clone(),
369 ));
370
371 let vector_index_service = if let Some(embedding_config) = &config.embedding {
373 Some(Arc::new(
374 crate::core::vector_index::VectorIndexServiceImpl::with_config(
375 embedding_config,
376 &config.skill_storage_path,
377 ),
378 )
379 as Arc<dyn crate::core::vector_index::VectorIndexService>)
380 } else {
381 None
382 };
383
384 let hot_reload_manager = if config.hot_reload.enabled {
386 Some(Arc::new(crate::storage::hot_reload::HotReloadManager::new(
387 storage.clone(),
388 event_bus.clone(),
389 )?))
390 } else {
391 None
392 };
393
394 Ok(Self {
395 config,
396 skill_manager,
397 metadata_service,
398 vector_index_service,
399 storage,
400 zip_handler,
401 sandbox,
402 skill_validator,
403 zip_validator,
404 event_bus,
405 hot_reload_manager,
406 initialized: false,
407 })
408 }
409
410 pub async fn initialize(&mut self) -> Result<(), ServiceError> {
412 if self.initialized {
413 return Ok(());
414 }
415
416 info!("Initializing service components...");
417
418 self.storage.initialize().await?;
420
421 if let Some(hot_reload) = &self.hot_reload_manager {
423 hot_reload
424 .enable_hot_reloading(self.config.hot_reload.watch_paths.clone())
425 .await?;
426 }
427
428 self.auto_index_skills_from_filesystem().await?;
430
431 self.initialized = true;
432 info!("Service initialization complete");
433
434 Ok(())
435 }
436
437 pub async fn shutdown(&mut self) -> Result<(), ServiceError> {
439 info!("Shutting down service...");
440
441 if let Some(hot_reload) = &self.hot_reload_manager {
443 hot_reload.disable_hot_reloading().await?;
444 }
445
446 self.storage.clear_cache().await?;
448
449 self.initialized = false;
450 info!("Service shutdown complete");
451
452 Ok(())
453 }
454
455 pub fn skill_manager(&self) -> Arc<dyn crate::core::skill_manager::SkillManagementService> {
457 self.skill_manager.clone()
458 }
459
460 pub fn metadata_service(&self) -> Arc<dyn crate::core::metadata::MetadataService> {
462 self.metadata_service.clone()
463 }
464
465 pub fn vector_index_service(
467 &self,
468 ) -> Option<Arc<dyn crate::core::vector_index::VectorIndexService>> {
469 self.vector_index_service.clone()
470 }
471
472 pub fn loading_service(&self) -> Arc<dyn crate::core::loading::ProgressiveLoadingService> {
474 Arc::new(crate::core::loading::LoadingService::new())
475 }
476
477 pub fn tool_service(&self) -> Arc<dyn crate::core::tool_calling::ToolCallingService> {
479 Arc::new(crate::core::tool_calling::ToolCallingServiceImpl::new())
480 }
481
482 pub fn routing_service(&self) -> Arc<dyn crate::core::routing::RoutingService> {
484 Arc::new(crate::core::routing::RoutingServiceImpl::new(
485 self.metadata_service.clone(),
486 ))
487 }
488
489 pub fn config(&self) -> &ServiceConfig {
491 &self.config
492 }
493
494 pub fn context_resolver(&self) -> crate::core::context_resolver::ContextResolver {
496 crate::core::context_resolver::ContextResolver::new(
497 self.skill_manager.clone(),
498 self.metadata_service.clone(),
499 self.vector_index_service.clone(),
500 self.config.embedding.clone(),
501 self.config.skill_storage_path.clone(),
502 )
503 }
504
505 pub fn is_initialized(&self) -> bool {
507 self.initialized
508 }
509
510 async fn auto_index_skills_from_filesystem(&self) -> Result<(), ServiceError> {
512 use walkdir::WalkDir;
513
514 let mut indexed_count = 0;
515
516 for entry in WalkDir::new(&self.config.skill_storage_path)
518 .into_iter()
519 .filter_entry(|e| {
520 !e.file_name()
522 .to_str()
523 .map(|s| {
524 s.starts_with('.')
525 || s == "node_modules"
526 || s == "target"
527 || s == "__pycache__"
528 })
529 .unwrap_or(false)
530 })
531 {
532 let entry = entry.map_err(|e| {
533 ServiceError::Custom(format!("Failed to read directory entry: {}", e))
534 })?;
535
536 if entry.file_type().is_file() {
538 let fname = entry.file_name();
539 if fname == "SKILL.md" || fname == "skill.md" {
540 let skill_file = entry.path();
541
542 match self.try_index_skill_from_file(skill_file).await {
543 Ok(_) => {
544 indexed_count += 1;
545 }
546 Err(e) => {
547 tracing::warn!(
548 "Failed to index skill at {}: {}",
549 skill_file.display(),
550 e
551 );
552 }
553 }
554 }
555 }
556 }
557
558 if indexed_count > 0 {
559 info!("Auto-indexed {} skills from filesystem", indexed_count);
560 }
561
562 Ok(())
563 }
564
565 async fn try_index_skill_from_file(
567 &self,
568 skill_file: &std::path::Path,
569 ) -> Result<(), ServiceError> {
570 let content = tokio::fs::read_to_string(skill_file)
572 .await
573 .map_err(|e| ServiceError::Custom(format!("Failed to read SKILL.md: {}", e)))?;
574
575 let frontmatter = crate::core::metadata::parse_yaml_frontmatter(&content)?;
577
578 let skill_dir = skill_file
580 .parent()
581 .ok_or_else(|| ServiceError::Custom("SKILL.md has no parent directory".to_string()))?;
582
583 let skill_id_str = skill_dir
585 .file_name()
586 .and_then(|n| n.to_str())
587 .ok_or_else(|| ServiceError::Custom("Invalid skill directory name".to_string()))?
588 .to_string();
589 let skill_id = SkillId::new(skill_id_str)?;
590
591 let mut skill = crate::core::skill_manager::SkillDefinition::new(
593 skill_id.clone(),
594 frontmatter.name,
595 frontmatter.description,
596 frontmatter.version.unwrap_or_else(|| "1.0.0".to_string()),
597 );
598
599 skill.author = frontmatter.author;
601 skill.skill_file = skill_file.to_path_buf();
602
603 skill.created_at = chrono::Utc::now();
605 skill.updated_at = chrono::Utc::now();
606
607 match self.skill_manager.register_skill(skill).await {
609 Ok(_) => Ok(()),
610 Err(crate::core::service::ServiceError::Custom(msg))
611 if msg.contains("already exists") =>
612 {
613 Ok(())
615 }
616 Err(e) => Err(e),
617 }
618 }
619}
620
621#[cfg(test)]
622#[allow(clippy::unwrap_used)]
623mod tests {
624 use super::*;
625 use tempfile::TempDir;
626
627 #[tokio::test]
628 async fn test_service_creation() {
629 let temp_dir = TempDir::new().unwrap();
630 let config = ServiceConfig {
631 skill_storage_path: temp_dir.path().to_path_buf(),
632 ..Default::default()
633 };
634
635 let mut service = FastSkillService::new(config).await.unwrap();
636 assert!(!service.is_initialized());
637
638 service.initialize().await.unwrap();
639 assert!(service.is_initialized());
640 }
641
642 #[tokio::test]
643 async fn test_service_shutdown() {
644 let temp_dir = TempDir::new().unwrap();
645 let config = ServiceConfig {
646 skill_storage_path: temp_dir.path().to_path_buf(),
647 ..Default::default()
648 };
649
650 let mut service = FastSkillService::new(config).await.unwrap();
651 service.initialize().await.unwrap();
652
653 service.shutdown().await.unwrap();
654 assert!(!service.is_initialized());
655 }
656
657 #[test]
658 fn test_skill_id_new_validates_input() {
659 assert!(SkillId::new("valid-id".to_string()).is_ok());
660 assert!(SkillId::new("valid_id_123".to_string()).is_ok());
661 assert!(SkillId::new("".to_string()).is_err());
662 assert!(SkillId::new("bad/id".to_string()).is_err());
663 assert!(SkillId::new("id with spaces".to_string()).is_err());
664 }
665
666 #[test]
667 fn test_skill_id_try_from_validates_input() {
668 assert!(SkillId::try_from("valid-id".to_string()).is_ok());
670 assert!(SkillId::try_from("".to_string()).is_err());
671 assert!(SkillId::try_from("bad/id".to_string()).is_err());
672 }
673}