Skip to main content

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