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    #[test]
414    fn test_plugin_loader_error_types() {
415        let load_error = PluginLoaderError::LoadError {
416            message: "test error".to_string(),
417        };
418        assert!(matches!(load_error, PluginLoaderError::LoadError { .. }));
419
420        let validation_error = PluginLoaderError::ValidationError {
421            message: "validation failed".to_string(),
422        };
423        assert!(matches!(validation_error, PluginLoaderError::ValidationError { .. }));
424    }
425
426    #[test]
427    fn test_plugin_discovery_success() {
428        let plugin_id = PluginId("test-plugin".to_string());
429        let manifest = PluginManifest::new(PluginInfo::new(
430            plugin_id.clone(),
431            PluginVersion::new(1, 0, 0),
432            "Test Plugin",
433            "A test plugin",
434            PluginAuthor::new("test-author"),
435        ));
436
437        let result = PluginDiscovery::success(plugin_id, manifest, "/path/to/plugin".to_string());
438
439        assert!(result.is_success());
440        assert!(result.first_error().is_none());
441    }
442
443    #[test]
444    fn test_plugin_discovery_failure() {
445        let plugin_id = PluginId("failing-plugin".to_string());
446        let errors = vec!["Error 1".to_string(), "Error 2".to_string()];
447
448        let result =
449            PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors.clone());
450
451        assert!(!result.is_success());
452        assert_eq!(result.first_error(), Some("Error 1"));
453        assert_eq!(result.errors.len(), 2);
454    }
455
456    #[test]
457    fn test_module_exports() {
458        // Verify main types are accessible
459        let _ = std::marker::PhantomData::<PluginLoader>;
460        let _ = std::marker::PhantomData::<PluginRegistry>;
461        let _ = std::marker::PhantomData::<PluginValidator>;
462        // Compilation test - if this compiles, the types are properly defined
463    }
464}