Skip to main content

gestura_core_plugins/
plugin_system.rs

1//! Plugin system data model and manager for Gestura.
2//!
3//! Defines plugin metadata, lifecycle management, dependency resolution,
4//! permissions, and sandboxed execution. Re-exported by `gestura-core` so both
5//! the CLI and GUI share a single implementation and policy surface.
6
7use gestura_core_foundation::error::AppError;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13/// Plugin metadata.
14#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
15pub struct PluginMetadata {
16    pub id: String,
17    pub name: String,
18    pub version: String,
19    pub description: String,
20    pub author: String,
21    pub license: String,
22    pub homepage: Option<String>,
23    /// Optional URL of the plugin's source repository.
24    pub repository: Option<String>,
25    pub keywords: Vec<String>,
26    pub dependencies: Vec<PluginDependency>,
27    pub permissions: Vec<PluginPermission>,
28    pub entry_point: String,
29    pub supported_platforms: Vec<String>,
30    pub min_app_version: String,
31}
32
33/// Plugin dependency.
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct PluginDependency {
36    pub name: String,
37    pub version: String,
38    pub optional: bool,
39}
40
41/// Plugin permissions.
42#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
43pub enum PluginPermission {
44    /// File path pattern access.
45    FileSystem(String),
46    /// Network host pattern access.
47    Network(String),
48    VoiceAccess,
49    GestureAccess,
50    RingAccess,
51    SystemInfo,
52    Notifications,
53    ClipboardAccess,
54    ProcessSpawn,
55    DatabaseAccess,
56}
57
58/// Plugin state.
59#[derive(Debug, Clone, PartialEq, serde::Serialize)]
60pub enum PluginState {
61    Loaded,
62    Running,
63    Stopped,
64    Error(String),
65    Disabled,
66}
67
68/// Plugin instance.
69#[derive(Debug, Clone)]
70pub struct Plugin {
71    pub metadata: PluginMetadata,
72    pub state: PluginState,
73    pub path: PathBuf,
74    pub config: serde_json::Value,
75    pub last_error: Option<String>,
76    pub load_time: chrono::DateTime<chrono::Utc>,
77    pub last_activity: chrono::DateTime<chrono::Utc>,
78}
79
80/// Plugin API interface.
81pub trait PluginApi {
82    /// Initialize the plugin.
83    fn initialize(&mut self, config: serde_json::Value) -> Result<(), String>;
84
85    /// Start the plugin.
86    fn start(&mut self) -> Result<(), String>;
87
88    /// Stop the plugin.
89    fn stop(&mut self) -> Result<(), String>;
90
91    /// Handle a command.
92    fn handle_command(
93        &mut self,
94        command: &str,
95        args: serde_json::Value,
96    ) -> Result<serde_json::Value, String>;
97
98    /// Get plugin status.
99    fn get_status(&self) -> serde_json::Value;
100
101    /// Handle events.
102    fn handle_event(&mut self, event: &str, data: serde_json::Value) -> Result<(), String>;
103}
104
105/// Plugin manager.
106pub struct PluginManager {
107    plugins: Arc<RwLock<HashMap<String, Plugin>>>,
108    plugin_directory: PathBuf,
109    enabled_plugins: Arc<RwLock<Vec<String>>>,
110    event_handlers: Arc<RwLock<HashMap<String, Vec<String>>>>, // event -> plugin_ids
111    command_handlers: Arc<RwLock<HashMap<String, String>>>,    // command -> plugin_id
112}
113
114impl PluginManager {
115    /// Create a new plugin manager.
116    pub fn new(plugin_directory: PathBuf) -> Self {
117        Self {
118            plugins: Arc::new(RwLock::new(HashMap::new())),
119            plugin_directory,
120            enabled_plugins: Arc::new(RwLock::new(Vec::new())),
121            event_handlers: Arc::new(RwLock::new(HashMap::new())),
122            command_handlers: Arc::new(RwLock::new(HashMap::new())),
123        }
124    }
125
126    /// Discover plugins in the plugin directory.
127    pub async fn discover_plugins(&self) -> Result<Vec<PluginMetadata>, AppError> {
128        let mut discovered = Vec::new();
129
130        if !self.plugin_directory.exists() {
131            tokio::fs::create_dir_all(&self.plugin_directory)
132                .await
133                .map_err(AppError::Io)?;
134            return Ok(discovered);
135        }
136
137        let mut entries = tokio::fs::read_dir(&self.plugin_directory)
138            .await
139            .map_err(AppError::Io)?;
140
141        while let Some(entry) = entries.next_entry().await.map_err(AppError::Io)? {
142            let path = entry.path();
143
144            if path.is_dir() {
145                let manifest_path = path.join("plugin.json");
146                if manifest_path.exists() {
147                    match self.load_plugin_metadata(&manifest_path).await {
148                        Ok(metadata) => discovered.push(metadata),
149                        Err(e) => tracing::warn!(
150                            "Failed to load plugin metadata from {}: {}",
151                            manifest_path.display(),
152                            e
153                        ),
154                    }
155                }
156            }
157        }
158
159        tracing::info!("Discovered {} plugins", discovered.len());
160        Ok(discovered)
161    }
162
163    /// Load plugin metadata from manifest file.
164    async fn load_plugin_metadata(&self, manifest_path: &Path) -> Result<PluginMetadata, AppError> {
165        let content = tokio::fs::read_to_string(manifest_path)
166            .await
167            .map_err(AppError::Io)?;
168
169        let metadata: PluginMetadata = serde_json::from_str(&content)
170            .map_err(|e| AppError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
171
172        // Validate metadata.
173        self.validate_plugin_metadata(&metadata)?;
174
175        Ok(metadata)
176    }
177
178    /// Validate plugin metadata.
179    fn validate_plugin_metadata(&self, metadata: &PluginMetadata) -> Result<(), AppError> {
180        if metadata.id.trim().is_empty() {
181            return Err(AppError::Io(std::io::Error::new(
182                std::io::ErrorKind::InvalidData,
183                "Plugin ID cannot be empty",
184            )));
185        }
186
187        if metadata.name.trim().is_empty() {
188            return Err(AppError::Io(std::io::Error::new(
189                std::io::ErrorKind::InvalidData,
190                "Plugin name cannot be empty",
191            )));
192        }
193
194        if metadata.entry_point.trim().is_empty() {
195            return Err(AppError::Io(std::io::Error::new(
196                std::io::ErrorKind::InvalidData,
197                "Plugin entry point cannot be empty",
198            )));
199        }
200
201        // Validate version format (simplified).
202        if !metadata.version.contains('.') {
203            return Err(AppError::Io(std::io::Error::new(
204                std::io::ErrorKind::InvalidData,
205                "Invalid version format",
206            )));
207        }
208
209        Ok(())
210    }
211
212    /// Load a plugin.
213    pub async fn load_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
214        let plugin_path = self.plugin_directory.join(plugin_id);
215        let manifest_path = plugin_path.join("plugin.json");
216
217        if !manifest_path.exists() {
218            return Err(AppError::Io(std::io::Error::new(
219                std::io::ErrorKind::NotFound,
220                format!("Plugin manifest not found: {}", plugin_id),
221            )));
222        }
223
224        let metadata = self.load_plugin_metadata(&manifest_path).await?;
225
226        // Check if plugin is already loaded.
227        {
228            let plugins = self.plugins.read().await;
229            if plugins.contains_key(plugin_id) {
230                return Err(AppError::Io(std::io::Error::new(
231                    std::io::ErrorKind::AlreadyExists,
232                    format!("Plugin already loaded: {}", plugin_id),
233                )));
234            }
235        }
236
237        // Validate permissions.
238        self.validate_plugin_permissions(&metadata.permissions)
239            .await?;
240
241        // Create plugin instance.
242        let plugin = Plugin {
243            metadata: metadata.clone(),
244            state: PluginState::Loaded,
245            path: plugin_path,
246            config: serde_json::Value::Null,
247            last_error: None,
248            load_time: chrono::Utc::now(),
249            last_activity: chrono::Utc::now(),
250        };
251
252        // Store plugin.
253        let mut plugins = self.plugins.write().await;
254        plugins.insert(plugin_id.to_string(), plugin);
255
256        tracing::info!("Loaded plugin: {} v{}", metadata.name, metadata.version);
257        Ok(())
258    }
259
260    /// Validate plugin permissions.
261    async fn validate_plugin_permissions(
262        &self,
263        permissions: &[PluginPermission],
264    ) -> Result<(), AppError> {
265        for permission in permissions {
266            match permission {
267                PluginPermission::FileSystem(path)
268                    if path.contains("..") || path.starts_with('/') =>
269                {
270                    return Err(AppError::Io(std::io::Error::new(
271                        std::io::ErrorKind::PermissionDenied,
272                        "Invalid file system permission pattern",
273                    )));
274                }
275                PluginPermission::Network(host) if host == "*" => {
276                    return Err(AppError::Io(std::io::Error::new(
277                        std::io::ErrorKind::PermissionDenied,
278                        "Wildcard network access not allowed",
279                    )));
280                }
281                PluginPermission::ProcessSpawn => {
282                    // High-risk permission, require explicit approval.
283                    tracing::warn!("Plugin requests process spawn permission");
284                }
285                _ => {}
286            }
287        }
288        Ok(())
289    }
290
291    /// Start a plugin.
292    pub async fn start_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
293        let mut plugins = self.plugins.write().await;
294
295        let plugin = plugins.get_mut(plugin_id).ok_or_else(|| {
296            AppError::Io(std::io::Error::new(
297                std::io::ErrorKind::NotFound,
298                format!("Plugin not found: {}", plugin_id),
299            ))
300        })?;
301
302        if plugin.state == PluginState::Running {
303            return Ok(());
304        }
305
306        // Simulate plugin startup (in real implementation, would load and execute plugin code).
307        plugin.state = PluginState::Running;
308        plugin.last_activity = chrono::Utc::now();
309
310        tracing::info!("Started plugin: {}", plugin_id);
311        Ok(())
312    }
313
314    /// Stop a plugin.
315    pub async fn stop_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
316        let mut plugins = self.plugins.write().await;
317
318        let plugin = plugins.get_mut(plugin_id).ok_or_else(|| {
319            AppError::Io(std::io::Error::new(
320                std::io::ErrorKind::NotFound,
321                format!("Plugin not found: {}", plugin_id),
322            ))
323        })?;
324
325        if plugin.state == PluginState::Stopped {
326            return Ok(());
327        }
328
329        // Simulate plugin shutdown.
330        plugin.state = PluginState::Stopped;
331        plugin.last_activity = chrono::Utc::now();
332
333        tracing::info!("Stopped plugin: {}", plugin_id);
334        Ok(())
335    }
336
337    /// Unload a plugin.
338    pub async fn unload_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
339        // Stop plugin first.
340        self.stop_plugin(plugin_id).await?;
341
342        // Remove from collections.
343        let mut plugins = self.plugins.write().await;
344        let mut enabled = self.enabled_plugins.write().await;
345        let mut event_handlers = self.event_handlers.write().await;
346        let mut command_handlers = self.command_handlers.write().await;
347
348        plugins.remove(plugin_id);
349        enabled.retain(|id| id != plugin_id);
350
351        // Remove event handlers.
352        for handlers in event_handlers.values_mut() {
353            handlers.retain(|id| id != plugin_id);
354        }
355
356        // Remove command handlers.
357        command_handlers.retain(|_, id| id != plugin_id);
358
359        tracing::info!("Unloaded plugin: {}", plugin_id);
360        Ok(())
361    }
362
363    /// Execute plugin command.
364    pub async fn execute_command(
365        &self,
366        command: &str,
367        _args: serde_json::Value,
368    ) -> Result<serde_json::Value, AppError> {
369        let command_handlers = self.command_handlers.read().await;
370
371        if let Some(plugin_id) = command_handlers.get(command) {
372            // In real implementation, would call plugin's handle_command method.
373            tracing::info!("Executing command '{}' on plugin '{}'", command, plugin_id);
374
375            Ok(serde_json::json!({
376                "status": "success",
377                "plugin_id": plugin_id,
378                "command": command,
379                "result": "Command executed successfully"
380            }))
381        } else {
382            Err(AppError::Io(std::io::Error::new(
383                std::io::ErrorKind::NotFound,
384                format!("No handler found for command: {}", command),
385            )))
386        }
387    }
388
389    /// Broadcast event to plugins.
390    pub async fn broadcast_event(
391        &self,
392        event: &str,
393        _data: serde_json::Value,
394    ) -> Result<(), AppError> {
395        let event_handlers = self.event_handlers.read().await;
396
397        if let Some(handlers) = event_handlers.get(event) {
398            for plugin_id in handlers {
399                // In real implementation, would call plugin's handle_event method.
400                tracing::debug!("Broadcasting event '{}' to plugin '{}'", event, plugin_id);
401            }
402        }
403
404        Ok(())
405    }
406
407    /// Get all plugins.
408    pub async fn get_plugins(&self) -> Vec<Plugin> {
409        let plugins = self.plugins.read().await;
410        plugins.values().cloned().collect()
411    }
412
413    /// Get plugin by ID.
414    pub async fn get_plugin(&self, plugin_id: &str) -> Option<Plugin> {
415        let plugins = self.plugins.read().await;
416        plugins.get(plugin_id).cloned()
417    }
418
419    /// Get plugin statistics.
420    pub async fn get_stats(&self) -> serde_json::Value {
421        let plugins = self.plugins.read().await;
422        let enabled = self.enabled_plugins.read().await;
423
424        let total_plugins = plugins.len();
425        let running_plugins = plugins
426            .values()
427            .filter(|p| p.state == PluginState::Running)
428            .count();
429        let enabled_plugins = enabled.len();
430
431        serde_json::json!({
432            "total_plugins": total_plugins,
433            "running_plugins": running_plugins,
434            "enabled_plugins": enabled_plugins,
435            "plugin_directory": self.plugin_directory.display().to_string()
436        })
437    }
438}
439
440/// Global plugin manager instance.
441static PLUGIN_MANAGER: tokio::sync::OnceCell<PluginManager> = tokio::sync::OnceCell::const_new();
442
443/// Get the global plugin manager.
444pub async fn get_plugin_manager() -> &'static PluginManager {
445    PLUGIN_MANAGER
446        .get_or_init(|| async {
447            let plugin_dir = std::env::current_dir()
448                .unwrap_or_else(|_| PathBuf::from("."))
449                .join("plugins");
450            PluginManager::new(plugin_dir)
451        })
452        .await
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use tempfile::TempDir;
459
460    #[tokio::test]
461    async fn test_plugin_discovery() {
462        let temp_dir = TempDir::new().unwrap();
463        let manager = PluginManager::new(temp_dir.path().to_path_buf());
464
465        // Create a test plugin.
466        let plugin_dir = temp_dir.path().join("test-plugin");
467        tokio::fs::create_dir_all(&plugin_dir).await.unwrap();
468
469        let manifest = serde_json::json!({
470            "id": "test-plugin",
471            "name": "Test Plugin",
472            "version": "1.0.0",
473            "description": "A test plugin",
474            "author": "Test Author",
475            "license": "MIT",
476            "keywords": ["test"],
477            "dependencies": [],
478            "permissions": [],
479            "entry_point": "main.js",
480            "supported_platforms": ["linux", "macos", "windows"],
481            "min_app_version": "1.0.0"
482        });
483
484        tokio::fs::write(plugin_dir.join("plugin.json"), manifest.to_string())
485            .await
486            .unwrap();
487
488        let discovered = manager.discover_plugins().await.unwrap();
489        assert_eq!(discovered.len(), 1);
490        assert_eq!(discovered[0].id, "test-plugin");
491    }
492
493    #[tokio::test]
494    async fn test_plugin_loading() {
495        let temp_dir = TempDir::new().unwrap();
496        let manager = PluginManager::new(temp_dir.path().to_path_buf());
497
498        // Create and load test plugin.
499        let plugin_dir = temp_dir.path().join("test-plugin");
500        tokio::fs::create_dir_all(&plugin_dir).await.unwrap();
501
502        let manifest = serde_json::json!({
503            "id": "test-plugin",
504            "name": "Test Plugin",
505            "version": "1.0.0",
506            "description": "A test plugin",
507            "author": "Test Author",
508            "license": "MIT",
509            "keywords": ["test"],
510            "dependencies": [],
511            "permissions": [],
512            "entry_point": "main.js",
513            "supported_platforms": ["linux", "macos", "windows"],
514            "min_app_version": "1.0.0"
515        });
516
517        tokio::fs::write(plugin_dir.join("plugin.json"), manifest.to_string())
518            .await
519            .unwrap();
520
521        manager.load_plugin("test-plugin").await.unwrap();
522
523        let plugin = manager.get_plugin("test-plugin").await;
524        assert!(plugin.is_some());
525        assert_eq!(plugin.unwrap().state, PluginState::Loaded);
526    }
527}