Skip to main content

rustant_plugins/
lib.rs

1//! # Rustant Plugins
2//!
3//! Plugin system for the Rustant agent. Supports native dynamic loading (.so/.dll/.dylib)
4//! and WASM sandboxed plugins. Plugins can register tools, hooks, and channels.
5
6pub mod hooks;
7pub mod loader;
8pub mod security;
9pub mod wasm_loader;
10
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use tokio::sync::Mutex;
17
18pub use hooks::{Hook, HookContext, HookManager, HookPoint, HookResult};
19pub use loader::NativePluginLoader;
20pub use security::{PluginCapability, PluginSecurityValidator, SecurityValidationResult};
21pub use wasm_loader::WasmPluginLoader;
22
23/// Errors from plugin operations.
24#[derive(Debug, thiserror::Error)]
25pub enum PluginError {
26    #[error("Plugin not found: {0}")]
27    NotFound(String),
28    #[error("Plugin already loaded: {0}")]
29    AlreadyLoaded(String),
30    #[error("Failed to load plugin: {0}")]
31    LoadFailed(String),
32    #[error("Plugin version incompatible: {0}")]
33    VersionIncompatible(String),
34    #[error("Security validation failed: {0}")]
35    SecurityViolation(String),
36    #[error("Plugin execution error: {0}")]
37    ExecutionError(String),
38}
39
40/// Metadata about a plugin.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct PluginMetadata {
43    /// Plugin name.
44    pub name: String,
45    /// Plugin version.
46    pub version: String,
47    /// Human-readable description.
48    pub description: String,
49    /// Author.
50    pub author: Option<String>,
51    /// Required Rustant core version (semver range).
52    pub min_core_version: Option<String>,
53    /// Plugin capabilities.
54    pub capabilities: Vec<PluginCapability>,
55}
56
57/// The Plugin trait that all plugins must implement.
58#[async_trait]
59pub trait Plugin: Send + Sync {
60    /// Get plugin metadata.
61    fn metadata(&self) -> PluginMetadata;
62
63    /// Called when the plugin is loaded.
64    async fn on_load(&mut self) -> Result<(), PluginError>;
65
66    /// Called when the plugin is unloaded.
67    async fn on_unload(&mut self) -> Result<(), PluginError>;
68
69    /// Return tool definitions provided by this plugin.
70    fn tools(&self) -> Vec<PluginToolDef> {
71        Vec::new()
72    }
73
74    /// Return hooks this plugin wants to register.
75    fn hooks(&self) -> Vec<(HookPoint, Box<dyn Hook>)> {
76        Vec::new()
77    }
78}
79
80/// A tool definition from a plugin.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PluginToolDef {
83    /// Tool name.
84    pub name: String,
85    /// Tool description.
86    pub description: String,
87    /// JSON Schema for parameters.
88    pub parameters: serde_json::Value,
89}
90
91/// State of a loaded plugin.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct PluginState {
94    pub metadata: PluginMetadata,
95    pub loaded_at: chrono::DateTime<chrono::Utc>,
96    pub source_path: Option<String>,
97    pub plugin_type: PluginType,
98}
99
100/// Type of plugin.
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub enum PluginType {
103    Native,
104    Wasm,
105    Managed,
106}
107
108/// Manages plugin lifecycle: load, unload, list.
109pub struct PluginManager {
110    plugins: HashMap<String, PluginEntry>,
111    hook_manager: Arc<Mutex<HookManager>>,
112    plugins_dir: PathBuf,
113}
114
115struct PluginEntry {
116    plugin: Box<dyn Plugin>,
117    state: PluginState,
118}
119
120impl PluginManager {
121    /// Create a new plugin manager.
122    pub fn new(plugins_dir: impl Into<PathBuf>) -> Self {
123        Self {
124            plugins: HashMap::new(),
125            hook_manager: Arc::new(Mutex::new(HookManager::new())),
126            plugins_dir: plugins_dir.into(),
127        }
128    }
129
130    /// Get a reference to the hook manager.
131    pub fn hook_manager(&self) -> Arc<Mutex<HookManager>> {
132        self.hook_manager.clone()
133    }
134
135    /// Load a managed plugin (trait object).
136    pub async fn load_managed(&mut self, plugin: Box<dyn Plugin>) -> Result<(), PluginError> {
137        let metadata = plugin.metadata();
138        let name = metadata.name.clone();
139
140        if self.plugins.contains_key(&name) {
141            return Err(PluginError::AlreadyLoaded(name));
142        }
143
144        // Security validation
145        let validator = PluginSecurityValidator::new();
146        let validation = validator.validate(&metadata);
147        if !validation.is_valid {
148            return Err(PluginError::SecurityViolation(
149                validation
150                    .errors
151                    .first()
152                    .cloned()
153                    .unwrap_or_else(|| "Unknown security issue".into()),
154            ));
155        }
156
157        let mut plugin = plugin;
158        plugin.on_load().await?;
159
160        // Register hooks
161        let hooks = plugin.hooks();
162        {
163            let mut hm = self.hook_manager.lock().await;
164            for (point, hook) in hooks {
165                hm.register(point, hook);
166            }
167        }
168
169        let state = PluginState {
170            metadata: metadata.clone(),
171            loaded_at: chrono::Utc::now(),
172            source_path: None,
173            plugin_type: PluginType::Managed,
174        };
175
176        self.plugins.insert(name, PluginEntry { plugin, state });
177        Ok(())
178    }
179
180    /// Unload a plugin by name.
181    pub async fn unload(&mut self, name: &str) -> Result<(), PluginError> {
182        let mut entry = self
183            .plugins
184            .remove(name)
185            .ok_or_else(|| PluginError::NotFound(name.into()))?;
186        entry.plugin.on_unload().await?;
187        Ok(())
188    }
189
190    /// List all loaded plugins.
191    pub fn list(&self) -> Vec<&PluginState> {
192        self.plugins.values().map(|e| &e.state).collect()
193    }
194
195    /// Get a plugin state by name.
196    pub fn get(&self, name: &str) -> Option<&PluginState> {
197        self.plugins.get(name).map(|e| &e.state)
198    }
199
200    /// Number of loaded plugins.
201    pub fn len(&self) -> usize {
202        self.plugins.len()
203    }
204
205    /// Whether no plugins are loaded.
206    pub fn is_empty(&self) -> bool {
207        self.plugins.is_empty()
208    }
209
210    /// Get the plugins directory.
211    pub fn plugins_dir(&self) -> &Path {
212        &self.plugins_dir
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    struct MockPlugin {
221        name: String,
222        loaded: bool,
223    }
224
225    impl MockPlugin {
226        fn new(name: &str) -> Self {
227            Self {
228                name: name.into(),
229                loaded: false,
230            }
231        }
232    }
233
234    #[async_trait]
235    impl Plugin for MockPlugin {
236        fn metadata(&self) -> PluginMetadata {
237            PluginMetadata {
238                name: self.name.clone(),
239                version: "1.0.0".into(),
240                description: "A mock plugin".into(),
241                author: Some("Test".into()),
242                min_core_version: None,
243                capabilities: vec![],
244            }
245        }
246
247        async fn on_load(&mut self) -> Result<(), PluginError> {
248            self.loaded = true;
249            Ok(())
250        }
251
252        async fn on_unload(&mut self) -> Result<(), PluginError> {
253            self.loaded = false;
254            Ok(())
255        }
256    }
257
258    #[tokio::test]
259    async fn test_plugin_manager_load_unload() {
260        let mut mgr = PluginManager::new("/tmp/plugins");
261        let plugin = Box::new(MockPlugin::new("test-plugin"));
262
263        mgr.load_managed(plugin).await.unwrap();
264        assert_eq!(mgr.len(), 1);
265        assert!(!mgr.is_empty());
266
267        let state = mgr.get("test-plugin").unwrap();
268        assert_eq!(state.metadata.name, "test-plugin");
269        assert_eq!(state.plugin_type, PluginType::Managed);
270
271        mgr.unload("test-plugin").await.unwrap();
272        assert_eq!(mgr.len(), 0);
273        assert!(mgr.is_empty());
274    }
275
276    #[tokio::test]
277    async fn test_plugin_manager_duplicate_load() {
278        let mut mgr = PluginManager::new("/tmp/plugins");
279        let plugin1 = Box::new(MockPlugin::new("dupe"));
280        let plugin2 = Box::new(MockPlugin::new("dupe"));
281
282        mgr.load_managed(plugin1).await.unwrap();
283        let result = mgr.load_managed(plugin2).await;
284        assert!(matches!(result, Err(PluginError::AlreadyLoaded(_))));
285    }
286
287    #[tokio::test]
288    async fn test_plugin_manager_unload_not_found() {
289        let mut mgr = PluginManager::new("/tmp/plugins");
290        let result = mgr.unload("nonexistent").await;
291        assert!(matches!(result, Err(PluginError::NotFound(_))));
292    }
293
294    #[tokio::test]
295    async fn test_plugin_manager_list() {
296        let mut mgr = PluginManager::new("/tmp/plugins");
297        mgr.load_managed(Box::new(MockPlugin::new("alpha")))
298            .await
299            .unwrap();
300        mgr.load_managed(Box::new(MockPlugin::new("beta")))
301            .await
302            .unwrap();
303
304        let list = mgr.list();
305        assert_eq!(list.len(), 2);
306    }
307
308    #[test]
309    fn test_plugin_metadata_serialization() {
310        let meta = PluginMetadata {
311            name: "test".into(),
312            version: "1.0.0".into(),
313            description: "Test plugin".into(),
314            author: None,
315            min_core_version: Some("0.1.0".into()),
316            capabilities: vec![PluginCapability::ToolRegistration],
317        };
318        let json = serde_json::to_string(&meta).unwrap();
319        let restored: PluginMetadata = serde_json::from_str(&json).unwrap();
320        assert_eq!(restored.name, "test");
321    }
322}