use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
pub id: String,
pub name: String,
pub version: String,
pub description: Option<String>,
pub author: Option<PluginAuthor>,
pub homepage: Option<String>,
pub repository: Option<String>,
pub license: Option<String>,
pub csm_version: String,
pub main: String,
pub permissions: Vec<Permission>,
pub hooks: Vec<String>,
pub config_schema: Option<serde_json::Value>,
pub dependencies: Vec<PluginDependency>,
pub category: PluginCategory,
pub keywords: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginAuthor {
pub name: String,
pub email: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginDependency {
pub id: String,
pub version: String,
pub optional: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PluginCategory {
Provider,
Export,
Analysis,
Ui,
Automation,
Storage,
Auth,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Permission {
SessionRead,
SessionWrite,
SessionDelete,
ConfigRead,
ConfigWrite,
Network,
FileSystem,
Shell,
Sensitive,
Background,
Notifications,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PluginState {
Loaded,
Active,
Disabled,
Error,
Updating,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInstance {
pub manifest: PluginManifest,
pub state: PluginState,
pub path: PathBuf,
pub installed_at: DateTime<Utc>,
pub last_activated: Option<DateTime<Utc>>,
pub config: serde_json::Value,
pub error: Option<String>,
pub stats: PluginStats,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginStats {
pub activation_count: u64,
pub total_execution_ms: u64,
pub error_count: u64,
pub last_error: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PluginEvent {
SessionCreated { session_id: String },
SessionUpdated { session_id: String },
SessionDeleted { session_id: String },
SessionImported { session_id: String, provider: String },
SessionExported { session_id: String, format: String },
HarvestCompleted { session_count: usize, provider: String },
SyncCompleted { direction: String, changes: usize },
UserAction { action: String, context: serde_json::Value },
AppStartup,
AppShutdown,
ConfigChanged { key: String },
Custom { name: String, data: serde_json::Value },
}
#[derive(Debug, Clone)]
pub struct HookRegistration {
pub plugin_id: String,
pub event_pattern: String,
pub priority: i32,
pub handler_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookResult {
pub plugin_id: String,
pub success: bool,
pub data: Option<serde_json::Value>,
pub error: Option<String>,
pub execution_ms: u64,
}
pub struct PluginContext {
pub plugin_id: String,
pub permissions: Vec<Permission>,
pub data_dir: PathBuf,
pub config: serde_json::Value,
}
impl PluginContext {
pub fn has_permission(&self, permission: &Permission) -> bool {
self.permissions.contains(permission)
}
pub fn get_data_path(&self, filename: &str) -> PathBuf {
self.data_dir.join(filename)
}
pub fn read_data(&self, filename: &str) -> Result<String> {
if !self.has_permission(&Permission::FileSystem) {
return Err(anyhow!("Permission denied: FileSystem"));
}
let path = self.get_data_path(filename);
Ok(std::fs::read_to_string(path)?)
}
pub fn write_data(&self, filename: &str, content: &str) -> Result<()> {
if !self.has_permission(&Permission::FileSystem) {
return Err(anyhow!("Permission denied: FileSystem"));
}
std::fs::create_dir_all(&self.data_dir)?;
let path = self.get_data_path(filename);
Ok(std::fs::write(path, content)?)
}
}
pub struct PluginManager {
plugins_dir: PathBuf,
plugins: Arc<RwLock<HashMap<String, PluginInstance>>>,
hooks: Arc<RwLock<Vec<HookRegistration>>>,
configs: Arc<RwLock<HashMap<String, serde_json::Value>>>,
}
impl PluginManager {
pub fn new(plugins_dir: PathBuf) -> Self {
Self {
plugins_dir,
plugins: Arc::new(RwLock::new(HashMap::new())),
hooks: Arc::new(RwLock::new(Vec::new())),
configs: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn init(&self) -> Result<()> {
std::fs::create_dir_all(&self.plugins_dir)?;
self.discover_plugins().await?;
Ok(())
}
pub async fn discover_plugins(&self) -> Result<Vec<String>> {
let mut discovered = Vec::new();
if !self.plugins_dir.exists() {
return Ok(discovered);
}
for entry in std::fs::read_dir(&self.plugins_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let manifest_path = path.join("plugin.json");
if manifest_path.exists() {
match self.load_plugin(&path).await {
Ok(plugin_id) => discovered.push(plugin_id),
Err(e) => {
log::warn!("Failed to load plugin at {:?}: {}", path, e);
}
}
}
}
}
Ok(discovered)
}
pub async fn load_plugin(&self, plugin_path: &PathBuf) -> Result<String> {
let manifest_path = plugin_path.join("plugin.json");
let manifest_content = std::fs::read_to_string(&manifest_path)?;
let manifest: PluginManifest = serde_json::from_str(&manifest_content)?;
self.validate_manifest(&manifest)?;
self.check_dependencies(&manifest).await?;
let instance = PluginInstance {
manifest: manifest.clone(),
state: PluginState::Loaded,
path: plugin_path.clone(),
installed_at: Utc::now(),
last_activated: None,
config: serde_json::Value::Object(serde_json::Map::new()),
error: None,
stats: PluginStats::default(),
};
let plugin_id = manifest.id.clone();
self.plugins.write().await.insert(plugin_id.clone(), instance);
log::info!("Loaded plugin: {} v{}", manifest.name, manifest.version);
Ok(plugin_id)
}
fn validate_manifest(&self, manifest: &PluginManifest) -> Result<()> {
if manifest.id.is_empty() || manifest.id.len() > 64 {
return Err(anyhow!("Invalid plugin ID"));
}
if semver::Version::parse(&manifest.version).is_err() {
return Err(anyhow!("Invalid version format: {}", manifest.version));
}
let current_version = env!("CARGO_PKG_VERSION");
let req = semver::VersionReq::parse(&manifest.csm_version)
.map_err(|_| anyhow!("Invalid csm_version: {}", manifest.csm_version))?;
let current = semver::Version::parse(current_version)?;
if !req.matches(¤t) {
return Err(anyhow!(
"Plugin requires CSM {}, but current version is {}",
manifest.csm_version,
current_version
));
}
Ok(())
}
async fn check_dependencies(&self, manifest: &PluginManifest) -> Result<()> {
let plugins = self.plugins.read().await;
for dep in &manifest.dependencies {
if dep.optional {
continue;
}
let plugin = plugins.get(&dep.id);
match plugin {
None => {
return Err(anyhow!("Missing required dependency: {}", dep.id));
}
Some(p) => {
let req = semver::VersionReq::parse(&dep.version)?;
let ver = semver::Version::parse(&p.manifest.version)?;
if !req.matches(&ver) {
return Err(anyhow!(
"Dependency {} version {} does not match requirement {}",
dep.id, p.manifest.version, dep.version
));
}
}
}
}
Ok(())
}
pub async fn activate(&self, plugin_id: &str) -> Result<()> {
let mut plugins = self.plugins.write().await;
let plugin = plugins.get_mut(plugin_id)
.ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
if plugin.state == PluginState::Active {
return Ok(());
}
for hook in &plugin.manifest.hooks {
self.register_hook(plugin_id, hook, 0).await?;
}
plugin.state = PluginState::Active;
plugin.last_activated = Some(Utc::now());
plugin.stats.activation_count += 1;
log::info!("Activated plugin: {}", plugin_id);
Ok(())
}
pub async fn deactivate(&self, plugin_id: &str) -> Result<()> {
let mut plugins = self.plugins.write().await;
let plugin = plugins.get_mut(plugin_id)
.ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
self.unregister_hooks(plugin_id).await?;
plugin.state = PluginState::Disabled;
log::info!("Deactivated plugin: {}", plugin_id);
Ok(())
}
pub async fn uninstall(&self, plugin_id: &str) -> Result<()> {
self.deactivate(plugin_id).await.ok();
let mut plugins = self.plugins.write().await;
let plugin = plugins.remove(plugin_id)
.ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
if plugin.path.exists() {
std::fs::remove_dir_all(&plugin.path)?;
}
log::info!("Uninstalled plugin: {}", plugin_id);
Ok(())
}
async fn register_hook(&self, plugin_id: &str, event_pattern: &str, priority: i32) -> Result<()> {
let mut hooks = self.hooks.write().await;
hooks.push(HookRegistration {
plugin_id: plugin_id.to_string(),
event_pattern: event_pattern.to_string(),
priority,
handler_id: uuid::Uuid::new_v4().to_string(),
});
Ok(())
}
async fn unregister_hooks(&self, plugin_id: &str) -> Result<()> {
let mut hooks = self.hooks.write().await;
hooks.retain(|h| h.plugin_id != plugin_id);
Ok(())
}
pub async fn emit(&self, event: PluginEvent) -> Vec<HookResult> {
let hooks = self.hooks.read().await;
let plugins = self.plugins.read().await;
let mut results = Vec::new();
let event_name = self.get_event_name(&event);
let mut matching_hooks: Vec<_> = hooks.iter()
.filter(|h| self.matches_pattern(&h.event_pattern, &event_name))
.collect();
matching_hooks.sort_by_key(|h| h.priority);
for hook in matching_hooks {
let _plugin = match plugins.get(&hook.plugin_id) {
Some(p) if p.state == PluginState::Active => p,
_ => continue,
};
let start = std::time::Instant::now();
let result = HookResult {
plugin_id: hook.plugin_id.clone(),
success: true,
data: Some(serde_json::json!({
"event": event_name,
"handled": true
})),
error: None,
execution_ms: start.elapsed().as_millis() as u64,
};
results.push(result);
}
results
}
fn get_event_name(&self, event: &PluginEvent) -> String {
match event {
PluginEvent::SessionCreated { .. } => "session.created".to_string(),
PluginEvent::SessionUpdated { .. } => "session.updated".to_string(),
PluginEvent::SessionDeleted { .. } => "session.deleted".to_string(),
PluginEvent::SessionImported { .. } => "session.imported".to_string(),
PluginEvent::SessionExported { .. } => "session.exported".to_string(),
PluginEvent::HarvestCompleted { .. } => "harvest.completed".to_string(),
PluginEvent::SyncCompleted { .. } => "sync.completed".to_string(),
PluginEvent::UserAction { action, .. } => format!("user.{}", action),
PluginEvent::AppStartup => "app.startup".to_string(),
PluginEvent::AppShutdown => "app.shutdown".to_string(),
PluginEvent::ConfigChanged { .. } => "config.changed".to_string(),
PluginEvent::Custom { name, .. } => format!("custom.{}", name),
}
}
fn matches_pattern(&self, pattern: &str, event_name: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix("*") {
return event_name.starts_with(prefix);
}
pattern == event_name
}
pub async fn get_plugin(&self, plugin_id: &str) -> Option<PluginInstance> {
self.plugins.read().await.get(plugin_id).cloned()
}
pub async fn list_plugins(&self) -> Vec<PluginInstance> {
self.plugins.read().await.values().cloned().collect()
}
pub async fn get_config(&self, plugin_id: &str) -> Option<serde_json::Value> {
self.plugins.read().await
.get(plugin_id)
.map(|p| p.config.clone())
}
pub async fn set_config(&self, plugin_id: &str, config: serde_json::Value) -> Result<()> {
let mut plugins = self.plugins.write().await;
let plugin = plugins.get_mut(plugin_id)
.ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
if let Some(schema) = &plugin.manifest.config_schema {
self.validate_config(&config, schema)?;
}
plugin.config = config;
Ok(())
}
fn validate_config(&self, _config: &serde_json::Value, _schema: &serde_json::Value) -> Result<()> {
Ok(())
}
pub async fn create_context(&self, plugin_id: &str) -> Result<PluginContext> {
let plugins = self.plugins.read().await;
let plugin = plugins.get(plugin_id)
.ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
Ok(PluginContext {
plugin_id: plugin_id.to_string(),
permissions: plugin.manifest.permissions.clone(),
data_dir: plugin.path.join("data"),
config: plugin.config.clone(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryEntry {
pub id: String,
pub version: String,
pub name: String,
pub description: String,
pub author: String,
pub download_url: String,
pub downloads: u64,
pub rating: f32,
pub category: PluginCategory,
pub keywords: Vec<String>,
pub updated_at: DateTime<Utc>,
}
pub struct PluginRegistry {
registry_url: String,
}
impl PluginRegistry {
pub fn new(registry_url: String) -> Self {
Self { registry_url }
}
pub async fn search(&self, _query: &str, _category: Option<PluginCategory>) -> Result<Vec<RegistryEntry>> {
Ok(Vec::new())
}
pub async fn get_plugin(&self, plugin_id: &str) -> Result<RegistryEntry> {
Err(anyhow!("Plugin not found in registry: {}", plugin_id))
}
pub async fn install(&self, plugin_id: &str, manager: &PluginManager) -> Result<()> {
let _entry = self.get_plugin(plugin_id).await?;
let plugin_dir = manager.plugins_dir.join(plugin_id);
std::fs::create_dir_all(&plugin_dir)?;
manager.load_plugin(&plugin_dir).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_plugin_manager_init() {
let temp_dir = tempdir().unwrap();
let manager = PluginManager::new(temp_dir.path().to_path_buf());
assert!(manager.init().await.is_ok());
}
#[test]
fn test_event_name() {
let manager = PluginManager::new(PathBuf::from("."));
let event = PluginEvent::SessionCreated { session_id: "test".to_string() };
assert_eq!(manager.get_event_name(&event), "session.created");
}
#[test]
fn test_pattern_matching() {
let manager = PluginManager::new(PathBuf::from("."));
assert!(manager.matches_pattern("*", "session.created"));
assert!(manager.matches_pattern("session.*", "session.created"));
assert!(manager.matches_pattern("session.created", "session.created"));
assert!(!manager.matches_pattern("session.updated", "session.created"));
}
}