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 { message: String },
60
61    /// Plugin validation failed
62    #[error("Plugin validation error: {message}")]
63    ValidationError { message: String },
64
65    /// Security violation during plugin loading
66    #[error("Security violation: {violation}")]
67    SecurityViolation { violation: String },
68
69    /// Plugin manifest error
70    #[error("Plugin manifest error: {message}")]
71    ManifestError { message: String },
72
73    /// WebAssembly module error
74    #[error("WebAssembly module error: {message}")]
75    WasmError { message: String },
76
77    /// File system error
78    #[error("File system error: {message}")]
79    FsError { message: String },
80
81    /// Plugin already loaded
82    #[error("Plugin already loaded: {plugin_id}")]
83    AlreadyLoaded { plugin_id: PluginId },
84
85    /// Plugin not found
86    #[error("Plugin not found: {plugin_id}")]
87    NotFound { plugin_id: PluginId },
88
89    /// Plugin dependency error
90    #[error("Plugin dependency error: {message}")]
91    DependencyError { message: String },
92
93    /// Resource limit exceeded
94    #[error("Resource limit exceeded: {message}")]
95    ResourceLimit { message: String },
96
97    /// Plugin execution error
98    #[error("Plugin execution error: {message}")]
99    ExecutionError { message: String },
100}
101
102impl PluginLoaderError {
103    /// Create a load error
104    pub fn load<S: Into<String>>(message: S) -> Self {
105        Self::LoadError {
106            message: message.into(),
107        }
108    }
109
110    /// Create a validation error
111    pub fn validation<S: Into<String>>(message: S) -> Self {
112        Self::ValidationError {
113            message: message.into(),
114        }
115    }
116
117    /// Create a security violation error
118    pub fn security<S: Into<String>>(violation: S) -> Self {
119        Self::SecurityViolation {
120            violation: violation.into(),
121        }
122    }
123
124    /// Create a manifest error
125    pub fn manifest<S: Into<String>>(message: S) -> Self {
126        Self::ManifestError {
127            message: message.into(),
128        }
129    }
130
131    /// Create a WASM error
132    pub fn wasm<S: Into<String>>(message: S) -> Self {
133        Self::WasmError {
134            message: message.into(),
135        }
136    }
137
138    /// Create a file system error
139    pub fn fs<S: Into<String>>(message: S) -> Self {
140        Self::FsError {
141            message: message.into(),
142        }
143    }
144
145    /// Create an already loaded error
146    pub fn already_loaded(plugin_id: PluginId) -> Self {
147        Self::AlreadyLoaded { plugin_id }
148    }
149
150    /// Create a not found error
151    pub fn not_found(plugin_id: PluginId) -> Self {
152        Self::NotFound { plugin_id }
153    }
154
155    /// Create a dependency error
156    pub fn dependency<S: Into<String>>(message: S) -> Self {
157        Self::DependencyError {
158            message: message.into(),
159        }
160    }
161
162    /// Create a resource limit error
163    pub fn resource_limit<S: Into<String>>(message: S) -> Self {
164        Self::ResourceLimit {
165            message: message.into(),
166        }
167    }
168
169    /// Create an execution error
170    pub fn execution<S: Into<String>>(message: S) -> Self {
171        Self::ExecutionError {
172            message: message.into(),
173        }
174    }
175
176    /// Check if this is a security-related error
177    pub fn is_security_error(&self) -> bool {
178        matches!(self, PluginLoaderError::SecurityViolation { .. })
179    }
180}
181
182/// Plugin loader configuration
183#[derive(Debug, Clone)]
184pub struct PluginLoaderConfig {
185    /// Plugin directories to scan
186    pub plugin_dirs: Vec<String>,
187    /// Allow unsigned plugins (for development)
188    pub allow_unsigned: bool,
189    /// Trusted public keys for plugin signing (key IDs)
190    pub trusted_keys: Vec<String>,
191    /// Key data storage (key_id -> key_bytes)
192    pub key_data: std::collections::HashMap<String, Vec<u8>>,
193    /// Maximum plugins to load
194    pub max_plugins: usize,
195    /// Plugin loading timeout
196    pub load_timeout_secs: u64,
197    /// Enable debug logging
198    pub debug_logging: bool,
199    /// Skip WASM validation (for testing)
200    pub skip_wasm_validation: bool,
201}
202
203impl Default for PluginLoaderConfig {
204    fn default() -> Self {
205        Self {
206            plugin_dirs: vec!["~/.mockforge/plugins".to_string(), "./plugins".to_string()],
207            allow_unsigned: false,
208            trusted_keys: vec!["trusted-dev-key".to_string()],
209            key_data: std::collections::HashMap::new(),
210            max_plugins: 100,
211            load_timeout_secs: 30,
212            debug_logging: false,
213            skip_wasm_validation: false,
214        }
215    }
216}
217
218/// Plugin loading context
219#[derive(Debug, Clone)]
220pub struct PluginLoadContext {
221    /// Plugin ID
222    pub plugin_id: PluginId,
223    /// Plugin manifest
224    pub manifest: PluginManifest,
225    /// Plugin file path
226    pub plugin_path: String,
227    /// Loading timestamp
228    pub load_time: chrono::DateTime<chrono::Utc>,
229    /// Loader configuration
230    pub config: PluginLoaderConfig,
231}
232
233impl PluginLoadContext {
234    /// Create new loading context
235    pub fn new(
236        plugin_id: PluginId,
237        manifest: PluginManifest,
238        plugin_path: String,
239        config: PluginLoaderConfig,
240    ) -> Self {
241        Self {
242            plugin_id,
243            manifest,
244            plugin_path,
245            load_time: chrono::Utc::now(),
246            config,
247        }
248    }
249}
250
251/// Plugin loading statistics
252#[derive(Debug, Clone, Default)]
253pub struct PluginLoadStats {
254    /// Total plugins discovered
255    pub discovered: usize,
256    /// Plugins successfully loaded
257    pub loaded: usize,
258    /// Plugins that failed to load
259    pub failed: usize,
260    /// Plugins skipped due to validation
261    pub skipped: usize,
262    /// Loading start time
263    pub start_time: Option<chrono::DateTime<chrono::Utc>>,
264    /// Loading end time
265    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
266}
267
268impl PluginLoadStats {
269    /// Record loading start
270    pub fn start_loading(&mut self) {
271        self.start_time = Some(chrono::Utc::now());
272    }
273
274    /// Record loading completion
275    pub fn finish_loading(&mut self) {
276        self.end_time = Some(chrono::Utc::now());
277    }
278
279    /// Record successful plugin load
280    pub fn record_success(&mut self) {
281        self.loaded += 1;
282        self.discovered += 1;
283    }
284
285    /// Record failed plugin load
286    pub fn record_failure(&mut self) {
287        self.failed += 1;
288        self.discovered += 1;
289    }
290
291    /// Record skipped plugin
292    pub fn record_skipped(&mut self) {
293        self.skipped += 1;
294        self.discovered += 1;
295    }
296
297    /// Get loading duration
298    pub fn duration(&self) -> Option<chrono::Duration> {
299        match (self.start_time, self.end_time) {
300            (Some(start), Some(end)) => Some(end - start),
301            _ => None,
302        }
303    }
304
305    /// Get success rate as percentage
306    pub fn success_rate(&self) -> f64 {
307        if self.discovered == 0 {
308            1.0 // No plugins discovered means 100% success (no failures)
309        } else {
310            (self.loaded as f64 / self.discovered as f64) * 100.0
311        }
312    }
313
314    /// Get total number of plugins processed
315    pub fn total_plugins(&self) -> usize {
316        self.loaded + self.failed + self.skipped
317    }
318}
319
320/// Plugin discovery result
321#[derive(Debug, Clone)]
322pub struct PluginDiscovery {
323    /// Plugin ID
324    pub plugin_id: PluginId,
325    /// Plugin manifest
326    pub manifest: PluginManifest,
327    /// Plugin file path
328    pub path: String,
329    /// Whether plugin is valid
330    pub is_valid: bool,
331    /// Validation errors (if any)
332    pub errors: Vec<String>,
333}
334
335impl PluginDiscovery {
336    /// Create successful discovery
337    pub fn success(plugin_id: PluginId, manifest: PluginManifest, path: String) -> Self {
338        Self {
339            plugin_id,
340            manifest,
341            path,
342            is_valid: true,
343            errors: Vec::new(),
344        }
345    }
346
347    /// Create failed discovery
348    pub fn failure(plugin_id: PluginId, path: String, errors: Vec<String>) -> Self {
349        let plugin_id_clone = PluginId(plugin_id.0.clone());
350        Self {
351            plugin_id,
352            manifest: PluginManifest::new(PluginInfo::new(
353                plugin_id_clone,
354                PluginVersion::new(0, 0, 0),
355                "Unknown",
356                "Plugin failed to load",
357                PluginAuthor::new("unknown"),
358            )),
359            path,
360            is_valid: false,
361            errors,
362        }
363    }
364
365    /// Check if discovery was successful
366    pub fn is_success(&self) -> bool {
367        self.is_valid
368    }
369
370    /// Get first error (if any)
371    pub fn first_error(&self) -> Option<&str> {
372        self.errors.first().map(|s| s.as_str())
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_plugin_loader_error_types() {
382        let load_error = PluginLoaderError::LoadError {
383            message: "test error".to_string(),
384        };
385        assert!(matches!(load_error, PluginLoaderError::LoadError { .. }));
386
387        let validation_error = PluginLoaderError::ValidationError {
388            message: "validation failed".to_string(),
389        };
390        assert!(matches!(validation_error, PluginLoaderError::ValidationError { .. }));
391    }
392
393    #[test]
394    fn test_plugin_discovery_success() {
395        let plugin_id = PluginId("test-plugin".to_string());
396        let manifest = PluginManifest::new(PluginInfo::new(
397            plugin_id.clone(),
398            PluginVersion::new(1, 0, 0),
399            "Test Plugin",
400            "A test plugin",
401            PluginAuthor::new("test-author"),
402        ));
403
404        let result = PluginDiscovery::success(plugin_id, manifest, "/path/to/plugin".to_string());
405
406        assert!(result.is_success());
407        assert!(result.first_error().is_none());
408    }
409
410    #[test]
411    fn test_plugin_discovery_failure() {
412        let plugin_id = PluginId("failing-plugin".to_string());
413        let errors = vec!["Error 1".to_string(), "Error 2".to_string()];
414
415        let result =
416            PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors.clone());
417
418        assert!(!result.is_success());
419        assert_eq!(result.first_error(), Some("Error 1"));
420        assert_eq!(result.errors.len(), 2);
421    }
422
423    #[test]
424    fn test_module_exports() {
425        // Verify main types are accessible
426        let _ = std::marker::PhantomData::<PluginLoader>;
427        let _ = std::marker::PhantomData::<PluginRegistry>;
428        let _ = std::marker::PhantomData::<PluginValidator>;
429        // Compilation test - if this compiles, the types are properly defined
430    }
431}