1pub 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct PluginMetadata {
43 pub name: String,
45 pub version: String,
47 pub description: String,
49 pub author: Option<String>,
51 pub min_core_version: Option<String>,
53 pub capabilities: Vec<PluginCapability>,
55}
56
57#[async_trait]
59pub trait Plugin: Send + Sync {
60 fn metadata(&self) -> PluginMetadata;
62
63 async fn on_load(&mut self) -> Result<(), PluginError>;
65
66 async fn on_unload(&mut self) -> Result<(), PluginError>;
68
69 fn tools(&self) -> Vec<PluginToolDef> {
71 Vec::new()
72 }
73
74 fn hooks(&self) -> Vec<(HookPoint, Box<dyn Hook>)> {
76 Vec::new()
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PluginToolDef {
83 pub name: String,
85 pub description: String,
87 pub parameters: serde_json::Value,
89}
90
91#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub enum PluginType {
103 Native,
104 Wasm,
105 Managed,
106}
107
108pub 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 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 pub fn hook_manager(&self) -> Arc<Mutex<HookManager>> {
132 self.hook_manager.clone()
133 }
134
135 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 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 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 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 pub fn list(&self) -> Vec<&PluginState> {
192 self.plugins.values().map(|e| &e.state).collect()
193 }
194
195 pub fn get(&self, name: &str) -> Option<&PluginState> {
197 self.plugins.get(name).map(|e| &e.state)
198 }
199
200 pub fn len(&self) -> usize {
202 self.plugins.len()
203 }
204
205 pub fn is_empty(&self) -> bool {
207 self.plugins.is_empty()
208 }
209
210 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}