mockforge_plugin_loader/
lib.rs

1//! # MockForge Plugin Loader
2//!
3//! Secure plugin loading and validation system for MockForge.
4//! This crate provides the plugin loader that handles:
5//!
6//! - Plugin discovery and validation
7//! - Security sandboxing and capability checking
8//! - WebAssembly module loading and instantiation
9//! - Plugin lifecycle management
10//!
11//! ## Security Features
12//!
13//! - **WASM Sandboxing**: All plugins run in isolated WebAssembly environments
14//! - **Capability Validation**: Strict permission checking before plugin execution
15//! - **Resource Limits**: Memory, CPU, and execution time constraints
16//! - **Code Signing**: Optional plugin signature verification
17
18use std::path::Path;
19use std::path::PathBuf;
20
21// Import types from plugin core
22use mockforge_plugin_core::{
23    PluginAuthor, PluginId, PluginInfo, PluginInstance, PluginManifest, PluginVersion,
24};
25
26pub mod git;
27pub mod installer;
28pub mod loader;
29pub mod metadata;
30pub mod registry;
31pub mod remote;
32pub mod runtime_adapter;
33pub mod sandbox;
34pub mod signature;
35pub mod signature_gen;
36pub mod validator;
37
38/// Re-export commonly used types
39pub use git::*;
40pub use installer::*;
41pub use loader::*;
42pub use metadata::*;
43pub use registry::*;
44pub use remote::*;
45pub use runtime_adapter::*;
46pub use sandbox::*;
47pub use signature::*;
48pub use signature_gen::*;
49pub use validator::*;
50
51/// Plugin loader result type
52pub type LoaderResult<T> = std::result::Result<T, PluginLoaderError>;
53
54/// Plugin loader error types
55#[derive(Debug, thiserror::Error)]
56pub enum PluginLoaderError {
57    /// Plugin loading failed
58    #[error("Plugin loading error: {message}")]
59    LoadError {
60        /// Error message describing the load failure
61        message: String,
62    },
63
64    /// Plugin validation failed
65    #[error("Plugin validation error: {message}")]
66    ValidationError {
67        /// Error message describing the validation failure
68        message: String,
69    },
70
71    /// Security violation during plugin loading
72    #[error("Security violation: {violation}")]
73    SecurityViolation {
74        /// Description of the security violation
75        violation: String,
76    },
77
78    /// Plugin manifest error
79    #[error("Plugin manifest error: {message}")]
80    ManifestError {
81        /// Error message describing the manifest issue
82        message: String,
83    },
84
85    /// WebAssembly module error
86    #[error("WebAssembly module error: {message}")]
87    WasmError {
88        /// Error message describing the WASM issue
89        message: String,
90    },
91
92    /// File system error
93    #[error("File system error: {message}")]
94    FsError {
95        /// Error message describing the file system issue
96        message: String,
97    },
98
99    /// Plugin already loaded
100    #[error("Plugin already loaded: {plugin_id}")]
101    AlreadyLoaded {
102        /// ID of the plugin that is already loaded
103        plugin_id: PluginId,
104    },
105
106    /// Plugin not found
107    #[error("Plugin not found: {plugin_id}")]
108    NotFound {
109        /// ID of the plugin that was not found
110        plugin_id: PluginId,
111    },
112
113    /// Plugin dependency error
114    #[error("Plugin dependency error: {message}")]
115    DependencyError {
116        /// Error message describing the dependency issue
117        message: String,
118    },
119
120    /// Resource limit exceeded
121    #[error("Resource limit exceeded: {message}")]
122    ResourceLimit {
123        /// Error message describing the resource limit that was exceeded
124        message: String,
125    },
126
127    /// Plugin execution error
128    #[error("Plugin execution error: {message}")]
129    ExecutionError {
130        /// Error message describing the execution failure
131        message: String,
132    },
133}
134
135impl PluginLoaderError {
136    /// Create a load error
137    pub fn load<S: Into<String>>(message: S) -> Self {
138        Self::LoadError {
139            message: message.into(),
140        }
141    }
142
143    /// Create a validation error
144    pub fn validation<S: Into<String>>(message: S) -> Self {
145        Self::ValidationError {
146            message: message.into(),
147        }
148    }
149
150    /// Create a security violation error
151    pub fn security<S: Into<String>>(violation: S) -> Self {
152        Self::SecurityViolation {
153            violation: violation.into(),
154        }
155    }
156
157    /// Create a manifest error
158    pub fn manifest<S: Into<String>>(message: S) -> Self {
159        Self::ManifestError {
160            message: message.into(),
161        }
162    }
163
164    /// Create a WASM error
165    pub fn wasm<S: Into<String>>(message: S) -> Self {
166        Self::WasmError {
167            message: message.into(),
168        }
169    }
170
171    /// Create a file system error
172    pub fn fs<S: Into<String>>(message: S) -> Self {
173        Self::FsError {
174            message: message.into(),
175        }
176    }
177
178    /// Create an already loaded error
179    pub fn already_loaded(plugin_id: PluginId) -> Self {
180        Self::AlreadyLoaded { plugin_id }
181    }
182
183    /// Create a not found error
184    pub fn not_found(plugin_id: PluginId) -> Self {
185        Self::NotFound { plugin_id }
186    }
187
188    /// Create a dependency error
189    pub fn dependency<S: Into<String>>(message: S) -> Self {
190        Self::DependencyError {
191            message: message.into(),
192        }
193    }
194
195    /// Create a resource limit error
196    pub fn resource_limit<S: Into<String>>(message: S) -> Self {
197        Self::ResourceLimit {
198            message: message.into(),
199        }
200    }
201
202    /// Create an execution error
203    pub fn execution<S: Into<String>>(message: S) -> Self {
204        Self::ExecutionError {
205            message: message.into(),
206        }
207    }
208
209    /// Check if this is a security-related error
210    pub fn is_security_error(&self) -> bool {
211        matches!(self, PluginLoaderError::SecurityViolation { .. })
212    }
213}
214
215/// Plugin loader configuration
216#[derive(Debug, Clone)]
217pub struct PluginLoaderConfig {
218    /// Plugin directories to scan
219    pub plugin_dirs: Vec<String>,
220    /// Allow unsigned plugins (for development)
221    pub allow_unsigned: bool,
222    /// Trusted public keys for plugin signing (key IDs)
223    pub trusted_keys: Vec<String>,
224    /// Key data storage (key_id -> key_bytes)
225    pub key_data: std::collections::HashMap<String, Vec<u8>>,
226    /// Maximum plugins to load
227    pub max_plugins: usize,
228    /// Plugin loading timeout
229    pub load_timeout_secs: u64,
230    /// Enable debug logging
231    pub debug_logging: bool,
232    /// Skip WASM validation (for testing)
233    pub skip_wasm_validation: bool,
234}
235
236impl Default for PluginLoaderConfig {
237    fn default() -> Self {
238        Self {
239            plugin_dirs: vec!["~/.mockforge/plugins".to_string(), "./plugins".to_string()],
240            allow_unsigned: false,
241            trusted_keys: vec!["trusted-dev-key".to_string()],
242            key_data: std::collections::HashMap::new(),
243            max_plugins: 100,
244            load_timeout_secs: 30,
245            debug_logging: false,
246            skip_wasm_validation: false,
247        }
248    }
249}
250
251/// Plugin loading context
252#[derive(Debug, Clone)]
253pub struct PluginLoadContext {
254    /// Plugin ID
255    pub plugin_id: PluginId,
256    /// Plugin manifest
257    pub manifest: PluginManifest,
258    /// Plugin file path
259    pub plugin_path: String,
260    /// Loading timestamp
261    pub load_time: chrono::DateTime<chrono::Utc>,
262    /// Loader configuration
263    pub config: PluginLoaderConfig,
264}
265
266impl PluginLoadContext {
267    /// Create new loading context
268    pub fn new(
269        plugin_id: PluginId,
270        manifest: PluginManifest,
271        plugin_path: String,
272        config: PluginLoaderConfig,
273    ) -> Self {
274        Self {
275            plugin_id,
276            manifest,
277            plugin_path,
278            load_time: chrono::Utc::now(),
279            config,
280        }
281    }
282}
283
284/// Plugin loading statistics
285#[derive(Debug, Clone, Default)]
286pub struct PluginLoadStats {
287    /// Total plugins discovered
288    pub discovered: usize,
289    /// Plugins successfully loaded
290    pub loaded: usize,
291    /// Plugins that failed to load
292    pub failed: usize,
293    /// Plugins skipped due to validation
294    pub skipped: usize,
295    /// Loading start time
296    pub start_time: Option<chrono::DateTime<chrono::Utc>>,
297    /// Loading end time
298    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
299}
300
301impl PluginLoadStats {
302    /// Record loading start
303    pub fn start_loading(&mut self) {
304        self.start_time = Some(chrono::Utc::now());
305    }
306
307    /// Record loading completion
308    pub fn finish_loading(&mut self) {
309        self.end_time = Some(chrono::Utc::now());
310    }
311
312    /// Record successful plugin load
313    pub fn record_success(&mut self) {
314        self.loaded += 1;
315        self.discovered += 1;
316    }
317
318    /// Record failed plugin load
319    pub fn record_failure(&mut self) {
320        self.failed += 1;
321        self.discovered += 1;
322    }
323
324    /// Record skipped plugin
325    pub fn record_skipped(&mut self) {
326        self.skipped += 1;
327        self.discovered += 1;
328    }
329
330    /// Get loading duration
331    pub fn duration(&self) -> Option<chrono::Duration> {
332        match (self.start_time, self.end_time) {
333            (Some(start), Some(end)) => Some(end - start),
334            _ => None,
335        }
336    }
337
338    /// Get success rate as percentage
339    pub fn success_rate(&self) -> f64 {
340        if self.discovered == 0 {
341            1.0 // No plugins discovered means 100% success (no failures)
342        } else {
343            (self.loaded as f64 / self.discovered as f64) * 100.0
344        }
345    }
346
347    /// Get total number of plugins processed
348    pub fn total_plugins(&self) -> usize {
349        self.loaded + self.failed + self.skipped
350    }
351}
352
353/// Plugin discovery result
354#[derive(Debug, Clone)]
355pub struct PluginDiscovery {
356    /// Plugin ID
357    pub plugin_id: PluginId,
358    /// Plugin manifest
359    pub manifest: PluginManifest,
360    /// Plugin file path
361    pub path: String,
362    /// Whether plugin is valid
363    pub is_valid: bool,
364    /// Validation errors (if any)
365    pub errors: Vec<String>,
366}
367
368impl PluginDiscovery {
369    /// Create successful discovery
370    pub fn success(plugin_id: PluginId, manifest: PluginManifest, path: String) -> Self {
371        Self {
372            plugin_id,
373            manifest,
374            path,
375            is_valid: true,
376            errors: Vec::new(),
377        }
378    }
379
380    /// Create failed discovery
381    pub fn failure(plugin_id: PluginId, path: String, errors: Vec<String>) -> Self {
382        let plugin_id_clone = PluginId(plugin_id.0.clone());
383        Self {
384            plugin_id,
385            manifest: PluginManifest::new(PluginInfo::new(
386                plugin_id_clone,
387                PluginVersion::new(0, 0, 0),
388                "Unknown",
389                "Plugin failed to load",
390                PluginAuthor::new("unknown"),
391            )),
392            path,
393            is_valid: false,
394            errors,
395        }
396    }
397
398    /// Check if discovery was successful
399    pub fn is_success(&self) -> bool {
400        self.is_valid
401    }
402
403    /// Get first error (if any)
404    pub fn first_error(&self) -> Option<&str> {
405        self.errors.first().map(|s| s.as_str())
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    // ===== PluginLoaderError Tests =====
414
415    #[test]
416    fn test_plugin_loader_error_types() {
417        let load_error = PluginLoaderError::LoadError {
418            message: "test error".to_string(),
419        };
420        assert!(matches!(load_error, PluginLoaderError::LoadError { .. }));
421
422        let validation_error = PluginLoaderError::ValidationError {
423            message: "validation failed".to_string(),
424        };
425        assert!(matches!(validation_error, PluginLoaderError::ValidationError { .. }));
426    }
427
428    #[test]
429    fn test_error_helper_constructors() {
430        let load_err = PluginLoaderError::load("load failed");
431        assert!(matches!(load_err, PluginLoaderError::LoadError { .. }));
432
433        let validation_err = PluginLoaderError::validation("validation failed");
434        assert!(matches!(validation_err, PluginLoaderError::ValidationError { .. }));
435
436        let security_err = PluginLoaderError::security("security violation");
437        assert!(matches!(security_err, PluginLoaderError::SecurityViolation { .. }));
438
439        let manifest_err = PluginLoaderError::manifest("manifest error");
440        assert!(matches!(manifest_err, PluginLoaderError::ManifestError { .. }));
441
442        let wasm_err = PluginLoaderError::wasm("wasm error");
443        assert!(matches!(wasm_err, PluginLoaderError::WasmError { .. }));
444
445        let fs_err = PluginLoaderError::fs("fs error");
446        assert!(matches!(fs_err, PluginLoaderError::FsError { .. }));
447
448        let dep_err = PluginLoaderError::dependency("dependency error");
449        assert!(matches!(dep_err, PluginLoaderError::DependencyError { .. }));
450
451        let resource_err = PluginLoaderError::resource_limit("resource limit");
452        assert!(matches!(resource_err, PluginLoaderError::ResourceLimit { .. }));
453
454        let exec_err = PluginLoaderError::execution("execution error");
455        assert!(matches!(exec_err, PluginLoaderError::ExecutionError { .. }));
456    }
457
458    #[test]
459    fn test_error_already_loaded() {
460        let plugin_id = PluginId::new("test-plugin");
461        let err = PluginLoaderError::already_loaded(plugin_id.clone());
462        assert!(matches!(err, PluginLoaderError::AlreadyLoaded { .. }));
463        assert_eq!(err.to_string(), format!("Plugin already loaded: {}", plugin_id));
464    }
465
466    #[test]
467    fn test_error_not_found() {
468        let plugin_id = PluginId::new("missing-plugin");
469        let err = PluginLoaderError::not_found(plugin_id.clone());
470        assert!(matches!(err, PluginLoaderError::NotFound { .. }));
471        assert_eq!(err.to_string(), format!("Plugin not found: {}", plugin_id));
472    }
473
474    #[test]
475    fn test_is_security_error() {
476        let security_err = PluginLoaderError::security("test");
477        assert!(security_err.is_security_error());
478
479        let load_err = PluginLoaderError::load("test");
480        assert!(!load_err.is_security_error());
481    }
482
483    #[test]
484    fn test_error_display() {
485        let err = PluginLoaderError::load("test message");
486        let err_str = err.to_string();
487        assert!(err_str.contains("Plugin loading error"));
488        assert!(err_str.contains("test message"));
489    }
490
491    // ===== PluginLoaderConfig Tests =====
492
493    #[test]
494    fn test_plugin_loader_config_default() {
495        let config = PluginLoaderConfig::default();
496        assert_eq!(config.plugin_dirs.len(), 2);
497        assert!(!config.allow_unsigned);
498        assert_eq!(config.max_plugins, 100);
499        assert_eq!(config.load_timeout_secs, 30);
500        assert!(!config.debug_logging);
501        assert!(!config.skip_wasm_validation);
502    }
503
504    #[test]
505    fn test_plugin_loader_config_clone() {
506        let config = PluginLoaderConfig::default();
507        let cloned = config.clone();
508        assert_eq!(config.max_plugins, cloned.max_plugins);
509        assert_eq!(config.load_timeout_secs, cloned.load_timeout_secs);
510    }
511
512    // ===== PluginLoadContext Tests =====
513
514    #[test]
515    fn test_plugin_load_context_creation() {
516        let plugin_id = PluginId::new("test-plugin");
517        let manifest = PluginManifest::new(PluginInfo::new(
518            plugin_id.clone(),
519            PluginVersion::new(1, 0, 0),
520            "Test Plugin",
521            "A test plugin",
522            PluginAuthor::new("test-author"),
523        ));
524        let config = PluginLoaderConfig::default();
525
526        let context = PluginLoadContext::new(
527            plugin_id.clone(),
528            manifest.clone(),
529            "/tmp/plugin".to_string(),
530            config.clone(),
531        );
532
533        assert_eq!(context.plugin_id, plugin_id);
534        assert_eq!(context.plugin_path, "/tmp/plugin");
535        assert_eq!(context.config.max_plugins, config.max_plugins);
536    }
537
538    // ===== PluginLoadStats Tests =====
539
540    #[test]
541    fn test_plugin_load_stats_default() {
542        let stats = PluginLoadStats::default();
543        assert_eq!(stats.discovered, 0);
544        assert_eq!(stats.loaded, 0);
545        assert_eq!(stats.failed, 0);
546        assert_eq!(stats.skipped, 0);
547        assert!(stats.start_time.is_none());
548        assert!(stats.end_time.is_none());
549    }
550
551    #[test]
552    fn test_plugin_load_stats_timing() {
553        let mut stats = PluginLoadStats::default();
554        assert!(stats.duration().is_none());
555
556        stats.start_loading();
557        assert!(stats.start_time.is_some());
558        assert!(stats.duration().is_none());
559
560        std::thread::sleep(std::time::Duration::from_millis(10));
561
562        stats.finish_loading();
563        assert!(stats.end_time.is_some());
564        assert!(stats.duration().is_some());
565        let duration = stats.duration().unwrap();
566        assert!(duration.num_milliseconds() >= 10);
567    }
568
569    #[test]
570    fn test_plugin_load_stats_record_success() {
571        let mut stats = PluginLoadStats::default();
572        stats.record_success();
573        assert_eq!(stats.loaded, 1);
574        assert_eq!(stats.discovered, 1);
575
576        stats.record_success();
577        assert_eq!(stats.loaded, 2);
578        assert_eq!(stats.discovered, 2);
579    }
580
581    #[test]
582    fn test_plugin_load_stats_record_failure() {
583        let mut stats = PluginLoadStats::default();
584        stats.record_failure();
585        assert_eq!(stats.failed, 1);
586        assert_eq!(stats.discovered, 1);
587    }
588
589    #[test]
590    fn test_plugin_load_stats_record_skipped() {
591        let mut stats = PluginLoadStats::default();
592        stats.record_skipped();
593        assert_eq!(stats.skipped, 1);
594        assert_eq!(stats.discovered, 1);
595    }
596
597    #[test]
598    fn test_plugin_load_stats_success_rate() {
599        let mut stats = PluginLoadStats::default();
600        // No plugins discovered = 1.0 (implementation returns 1.0, not 100.0)
601        assert_eq!(stats.success_rate(), 1.0);
602
603        stats.record_success();
604        stats.record_success();
605        stats.record_failure();
606        stats.record_skipped();
607        // 2 loaded / 4 discovered = 50%
608        assert_eq!(stats.success_rate(), 50.0);
609    }
610
611    #[test]
612    fn test_plugin_load_stats_total_plugins() {
613        let mut stats = PluginLoadStats::default();
614        assert_eq!(stats.total_plugins(), 0);
615
616        stats.record_success();
617        stats.record_failure();
618        stats.record_skipped();
619        assert_eq!(stats.total_plugins(), 3);
620    }
621
622    #[test]
623    fn test_plugin_load_stats_clone() {
624        let mut stats = PluginLoadStats::default();
625        stats.record_success();
626        stats.start_loading();
627
628        let cloned = stats.clone();
629        assert_eq!(cloned.loaded, stats.loaded);
630        assert_eq!(cloned.discovered, stats.discovered);
631        assert_eq!(cloned.start_time, stats.start_time);
632    }
633
634    // ===== PluginDiscovery Tests =====
635
636    #[test]
637    fn test_plugin_discovery_success() {
638        let plugin_id = PluginId("test-plugin".to_string());
639        let manifest = PluginManifest::new(PluginInfo::new(
640            plugin_id.clone(),
641            PluginVersion::new(1, 0, 0),
642            "Test Plugin",
643            "A test plugin",
644            PluginAuthor::new("test-author"),
645        ));
646
647        let result = PluginDiscovery::success(plugin_id, manifest, "/path/to/plugin".to_string());
648
649        assert!(result.is_success());
650        assert!(result.first_error().is_none());
651        assert!(result.is_valid);
652        assert!(result.errors.is_empty());
653    }
654
655    #[test]
656    fn test_plugin_discovery_failure() {
657        let plugin_id = PluginId("failing-plugin".to_string());
658        let errors = vec!["Error 1".to_string(), "Error 2".to_string()];
659
660        let result =
661            PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors.clone());
662
663        assert!(!result.is_success());
664        assert_eq!(result.first_error(), Some("Error 1"));
665        assert_eq!(result.errors.len(), 2);
666        assert!(!result.is_valid);
667    }
668
669    #[test]
670    fn test_plugin_discovery_failure_with_empty_errors() {
671        let plugin_id = PluginId("failing-plugin".to_string());
672        let errors = vec![];
673
674        let result = PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors);
675
676        assert!(!result.is_success());
677        assert!(result.first_error().is_none());
678        assert!(result.errors.is_empty());
679    }
680
681    #[test]
682    fn test_plugin_discovery_clone() {
683        let plugin_id = PluginId("test-plugin".to_string());
684        let manifest = PluginManifest::new(PluginInfo::new(
685            plugin_id.clone(),
686            PluginVersion::new(1, 0, 0),
687            "Test",
688            "Test",
689            PluginAuthor::new("test"),
690        ));
691
692        let discovery = PluginDiscovery::success(plugin_id, manifest, "/path".to_string());
693        let cloned = discovery.clone();
694
695        assert_eq!(discovery.plugin_id, cloned.plugin_id);
696        assert_eq!(discovery.is_valid, cloned.is_valid);
697    }
698
699    // ===== Module Exports Tests =====
700
701    #[test]
702    fn test_module_exports() {
703        // Verify main types are accessible
704        let _ = std::marker::PhantomData::<PluginLoader>;
705        let _ = std::marker::PhantomData::<PluginRegistry>;
706        let _ = std::marker::PhantomData::<PluginValidator>;
707        // Compilation test - if this compiles, the types are properly defined
708    }
709
710    #[test]
711    fn test_loader_result_type() {
712        let success: LoaderResult<i32> = Ok(42);
713        assert!(success.is_ok());
714        assert_eq!(success.unwrap(), 42);
715
716        let error: LoaderResult<i32> = Err(PluginLoaderError::load("test"));
717        assert!(error.is_err());
718    }
719}