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                    // Validate file system access patterns.
269                    if path.contains("..") || path.starts_with('/') {
270                        return Err(AppError::Io(std::io::Error::new(
271                            std::io::ErrorKind::PermissionDenied,
272                            "Invalid file system permission pattern",
273                        )));
274                    }
275                }
276                PluginPermission::Network(host) => {
277                    // Validate network access patterns.
278                    if host == "*" {
279                        return Err(AppError::Io(std::io::Error::new(
280                            std::io::ErrorKind::PermissionDenied,
281                            "Wildcard network access not allowed",
282                        )));
283                    }
284                }
285                PluginPermission::ProcessSpawn => {
286                    // High-risk permission, require explicit approval.
287                    tracing::warn!("Plugin requests process spawn permission");
288                }
289                _ => {}
290            }
291        }
292        Ok(())
293    }
294
295    /// Start a plugin.
296    pub async fn start_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
297        let mut plugins = self.plugins.write().await;
298
299        let plugin = plugins.get_mut(plugin_id).ok_or_else(|| {
300            AppError::Io(std::io::Error::new(
301                std::io::ErrorKind::NotFound,
302                format!("Plugin not found: {}", plugin_id),
303            ))
304        })?;
305
306        if plugin.state == PluginState::Running {
307            return Ok(());
308        }
309
310        // Simulate plugin startup (in real implementation, would load and execute plugin code).
311        plugin.state = PluginState::Running;
312        plugin.last_activity = chrono::Utc::now();
313
314        tracing::info!("Started plugin: {}", plugin_id);
315        Ok(())
316    }
317
318    /// Stop a plugin.
319    pub async fn stop_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
320        let mut plugins = self.plugins.write().await;
321
322        let plugin = plugins.get_mut(plugin_id).ok_or_else(|| {
323            AppError::Io(std::io::Error::new(
324                std::io::ErrorKind::NotFound,
325                format!("Plugin not found: {}", plugin_id),
326            ))
327        })?;
328
329        if plugin.state == PluginState::Stopped {
330            return Ok(());
331        }
332
333        // Simulate plugin shutdown.
334        plugin.state = PluginState::Stopped;
335        plugin.last_activity = chrono::Utc::now();
336
337        tracing::info!("Stopped plugin: {}", plugin_id);
338        Ok(())
339    }
340
341    /// Unload a plugin.
342    pub async fn unload_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
343        // Stop plugin first.
344        self.stop_plugin(plugin_id).await?;
345
346        // Remove from collections.
347        let mut plugins = self.plugins.write().await;
348        let mut enabled = self.enabled_plugins.write().await;
349        let mut event_handlers = self.event_handlers.write().await;
350        let mut command_handlers = self.command_handlers.write().await;
351
352        plugins.remove(plugin_id);
353        enabled.retain(|id| id != plugin_id);
354
355        // Remove event handlers.
356        for handlers in event_handlers.values_mut() {
357            handlers.retain(|id| id != plugin_id);
358        }
359
360        // Remove command handlers.
361        command_handlers.retain(|_, id| id != plugin_id);
362
363        tracing::info!("Unloaded plugin: {}", plugin_id);
364        Ok(())
365    }
366
367    /// Execute plugin command.
368    pub async fn execute_command(
369        &self,
370        command: &str,
371        _args: serde_json::Value,
372    ) -> Result<serde_json::Value, AppError> {
373        let command_handlers = self.command_handlers.read().await;
374
375        if let Some(plugin_id) = command_handlers.get(command) {
376            // In real implementation, would call plugin's handle_command method.
377            tracing::info!("Executing command '{}' on plugin '{}'", command, plugin_id);
378
379            Ok(serde_json::json!({
380                "status": "success",
381                "plugin_id": plugin_id,
382                "command": command,
383                "result": "Command executed successfully"
384            }))
385        } else {
386            Err(AppError::Io(std::io::Error::new(
387                std::io::ErrorKind::NotFound,
388                format!("No handler found for command: {}", command),
389            )))
390        }
391    }
392
393    /// Broadcast event to plugins.
394    pub async fn broadcast_event(
395        &self,
396        event: &str,
397        _data: serde_json::Value,
398    ) -> Result<(), AppError> {
399        let event_handlers = self.event_handlers.read().await;
400
401        if let Some(handlers) = event_handlers.get(event) {
402            for plugin_id in handlers {
403                // In real implementation, would call plugin's handle_event method.
404                tracing::debug!("Broadcasting event '{}' to plugin '{}'", event, plugin_id);
405            }
406        }
407
408        Ok(())
409    }
410
411    /// Get all plugins.
412    pub async fn get_plugins(&self) -> Vec<Plugin> {
413        let plugins = self.plugins.read().await;
414        plugins.values().cloned().collect()
415    }
416
417    /// Get plugin by ID.
418    pub async fn get_plugin(&self, plugin_id: &str) -> Option<Plugin> {
419        let plugins = self.plugins.read().await;
420        plugins.get(plugin_id).cloned()
421    }
422
423    /// Get plugin statistics.
424    pub async fn get_stats(&self) -> serde_json::Value {
425        let plugins = self.plugins.read().await;
426        let enabled = self.enabled_plugins.read().await;
427
428        let total_plugins = plugins.len();
429        let running_plugins = plugins
430            .values()
431            .filter(|p| p.state == PluginState::Running)
432            .count();
433        let enabled_plugins = enabled.len();
434
435        serde_json::json!({
436            "total_plugins": total_plugins,
437            "running_plugins": running_plugins,
438            "enabled_plugins": enabled_plugins,
439            "plugin_directory": self.plugin_directory.display().to_string()
440        })
441    }
442}
443
444/// Global plugin manager instance.
445static PLUGIN_MANAGER: tokio::sync::OnceCell<PluginManager> = tokio::sync::OnceCell::const_new();
446
447/// Get the global plugin manager.
448pub async fn get_plugin_manager() -> &'static PluginManager {
449    PLUGIN_MANAGER
450        .get_or_init(|| async {
451            let plugin_dir = std::env::current_dir()
452                .unwrap_or_else(|_| PathBuf::from("."))
453                .join("plugins");
454            PluginManager::new(plugin_dir)
455        })
456        .await
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use tempfile::TempDir;
463
464    #[tokio::test]
465    async fn test_plugin_discovery() {
466        let temp_dir = TempDir::new().unwrap();
467        let manager = PluginManager::new(temp_dir.path().to_path_buf());
468
469        // Create a test plugin.
470        let plugin_dir = temp_dir.path().join("test-plugin");
471        tokio::fs::create_dir_all(&plugin_dir).await.unwrap();
472
473        let manifest = serde_json::json!({
474            "id": "test-plugin",
475            "name": "Test Plugin",
476            "version": "1.0.0",
477            "description": "A test plugin",
478            "author": "Test Author",
479            "license": "MIT",
480            "keywords": ["test"],
481            "dependencies": [],
482            "permissions": [],
483            "entry_point": "main.js",
484            "supported_platforms": ["linux", "macos", "windows"],
485            "min_app_version": "1.0.0"
486        });
487
488        tokio::fs::write(plugin_dir.join("plugin.json"), manifest.to_string())
489            .await
490            .unwrap();
491
492        let discovered = manager.discover_plugins().await.unwrap();
493        assert_eq!(discovered.len(), 1);
494        assert_eq!(discovered[0].id, "test-plugin");
495    }
496
497    #[tokio::test]
498    async fn test_plugin_loading() {
499        let temp_dir = TempDir::new().unwrap();
500        let manager = PluginManager::new(temp_dir.path().to_path_buf());
501
502        // Create and load test plugin.
503        let plugin_dir = temp_dir.path().join("test-plugin");
504        tokio::fs::create_dir_all(&plugin_dir).await.unwrap();
505
506        let manifest = serde_json::json!({
507            "id": "test-plugin",
508            "name": "Test Plugin",
509            "version": "1.0.0",
510            "description": "A test plugin",
511            "author": "Test Author",
512            "license": "MIT",
513            "keywords": ["test"],
514            "dependencies": [],
515            "permissions": [],
516            "entry_point": "main.js",
517            "supported_platforms": ["linux", "macos", "windows"],
518            "min_app_version": "1.0.0"
519        });
520
521        tokio::fs::write(plugin_dir.join("plugin.json"), manifest.to_string())
522            .await
523            .unwrap();
524
525        manager.load_plugin("test-plugin").await.unwrap();
526
527        let plugin = manager.get_plugin("test-plugin").await;
528        assert!(plugin.is_some());
529        assert_eq!(plugin.unwrap().state, PluginState::Loaded);
530    }
531}