Skip to main content

driven/modules/
mod.rs

1//! # Module System
2//!
3//! Extensible module system for Driven that allows custom agents, workflows,
4//! templates, and resources to be packaged and shared.
5//!
6//! ## Features
7//!
8//! - Module installation from path or URL
9//! - Dependency resolution and version management
10//! - Resource isolation to prevent conflicts
11//! - DX LLM format manifests
12//!
13//! ## Module Manifest Format
14//!
15//! Modules are defined using DX LLM format in a `module.dx` file:
16//!
17//! ```text
18//! # My Custom Module
19//! id|my-module
20//! nm|My Custom Module
21//! v|1.0.0
22//! desc|A custom module for specialized workflows
23//! author|Your Name
24//! license|MIT
25//!
26//! # Dependencies
27//! dep.base-module|^1.0.0
28//!
29//! # Resources
30//! agent.0|custom-agent
31//! workflow.0|custom-workflow
32//! template.0|custom-template
33//! ```
34//!
35//! ## Example Usage
36//!
37//! ```rust,ignore
38//! use driven::modules::{ModuleManager, Module};
39//!
40//! // Create a module manager
41//! let mut manager = ModuleManager::new(".driven/modules");
42//!
43//! // Load existing modules
44//! manager.load()?;
45//!
46//! // Install a new module
47//! manager.install(Path::new("./my-module"))?;
48//!
49//! // List installed modules
50//! for module in manager.list_installed() {
51//!     println!("{}: {} v{}", module.id, module.name, module.version);
52//! }
53//!
54//! // Get all agents from installed modules (namespaced)
55//! let agents = manager.get_all_agents();
56//! // Returns: ["my-module:custom-agent", ...]
57//! ```
58//!
59//! ## Resource Isolation
60//!
61//! When isolation is enabled (default), all module resources are namespaced
62//! with the module ID to prevent conflicts:
63//!
64//! - Agent `my-agent` in module `my-module` becomes `my-module:my-agent`
65//! - Workflow `my-flow` in module `my-module` becomes `my-module:my-flow`
66//!
67//! ## Dependency Resolution
68//!
69//! The module system supports:
70//!
71//! - Exact version matching: `1.0.0`
72//! - Caret requirements: `^1.0.0` (compatible with major version)
73//! - Tilde requirements: `~1.0.0` (compatible with minor version)
74//! - Wildcard: `*` (any version)
75//! - Optional dependencies: marked with `optional: true`
76
77use serde::{Deserialize, Serialize};
78use std::collections::HashMap;
79use std::path::{Path, PathBuf};
80
81use crate::{DrivenError, Result};
82
83#[cfg(test)]
84mod property_tests;
85
86/// A Driven module containing agents, workflows, templates, and resources
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct Module {
89    /// Unique identifier for the module
90    pub id: String,
91    /// Human-readable name
92    pub name: String,
93    /// Semantic version
94    pub version: String,
95    /// Description of the module
96    pub description: String,
97    /// Author information
98    pub author: Option<String>,
99    /// License identifier
100    pub license: Option<String>,
101    /// Module dependencies
102    pub dependencies: Vec<ModuleDependency>,
103    /// Agent definitions provided by this module
104    pub agents: Vec<String>,
105    /// Workflow definitions provided by this module
106    pub workflows: Vec<String>,
107    /// Template definitions provided by this module
108    pub templates: Vec<String>,
109    /// Additional resources (files, configs, etc.)
110    pub resources: Vec<String>,
111    /// Installation path (set after installation)
112    #[serde(skip)]
113    pub install_path: Option<PathBuf>,
114}
115
116impl Module {
117    /// Create a new module with required fields
118    pub fn new(id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
119        Self {
120            id: id.into(),
121            name: name.into(),
122            version: version.into(),
123            description: String::new(),
124            author: None,
125            license: None,
126            dependencies: Vec::new(),
127            agents: Vec::new(),
128            workflows: Vec::new(),
129            templates: Vec::new(),
130            resources: Vec::new(),
131            install_path: None,
132        }
133    }
134
135    /// Set the description
136    pub fn with_description(mut self, description: impl Into<String>) -> Self {
137        self.description = description.into();
138        self
139    }
140
141    /// Add a dependency
142    pub fn with_dependency(mut self, dep: ModuleDependency) -> Self {
143        self.dependencies.push(dep);
144        self
145    }
146
147    /// Add an agent
148    pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
149        self.agents.push(agent.into());
150        self
151    }
152
153    /// Add a workflow
154    pub fn with_workflow(mut self, workflow: impl Into<String>) -> Self {
155        self.workflows.push(workflow.into());
156        self
157    }
158
159    /// Add a template
160    pub fn with_template(mut self, template: impl Into<String>) -> Self {
161        self.templates.push(template.into());
162        self
163    }
164
165    /// Add a resource
166    pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
167        self.resources.push(resource.into());
168        self
169    }
170
171    /// Get the namespaced ID for an agent
172    pub fn namespaced_agent(&self, agent: &str) -> String {
173        format!("{}:{}", self.id, agent)
174    }
175
176    /// Get the namespaced ID for a workflow
177    pub fn namespaced_workflow(&self, workflow: &str) -> String {
178        format!("{}:{}", self.id, workflow)
179    }
180
181    /// Get the namespaced ID for a template
182    pub fn namespaced_template(&self, template: &str) -> String {
183        format!("{}:{}", self.id, template)
184    }
185
186    /// Check if this module satisfies a version requirement
187    pub fn satisfies_version(&self, version_req: &str) -> bool {
188        // Simple version matching - supports exact, ^, ~, and * patterns
189        if version_req == "*" {
190            return true;
191        }
192        if version_req.starts_with('^') {
193            // Compatible with major version
194            let req = version_req.trim_start_matches('^');
195            return self
196                .version
197                .starts_with(&req.split('.').next().unwrap_or("0").to_string());
198        }
199        if version_req.starts_with('~') {
200            // Compatible with minor version
201            let req = version_req.trim_start_matches('~');
202            let parts: Vec<&str> = req.split('.').collect();
203            let self_parts: Vec<&str> = self.version.split('.').collect();
204            return parts.first() == self_parts.first() && parts.get(1) == self_parts.get(1);
205        }
206        // Exact match
207        self.version == version_req
208    }
209}
210
211/// A module dependency with version requirement
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
213pub struct ModuleDependency {
214    /// Module ID of the dependency
215    pub module_id: String,
216    /// Version requirement (semver-like)
217    pub version_req: String,
218    /// Whether this dependency is optional
219    #[serde(default)]
220    pub optional: bool,
221}
222
223impl ModuleDependency {
224    /// Create a new required dependency
225    pub fn new(module_id: impl Into<String>, version_req: impl Into<String>) -> Self {
226        Self {
227            module_id: module_id.into(),
228            version_req: version_req.into(),
229            optional: false,
230        }
231    }
232
233    /// Create an optional dependency
234    pub fn optional(module_id: impl Into<String>, version_req: impl Into<String>) -> Self {
235        Self {
236            module_id: module_id.into(),
237            version_req: version_req.into(),
238            optional: true,
239        }
240    }
241}
242
243/// Module installation status
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum ModuleStatus {
246    /// Module is installed and ready
247    Installed,
248    /// Module is being installed
249    Installing,
250    /// Module has pending updates
251    UpdateAvailable,
252    /// Module has unmet dependencies
253    DependencyError,
254    /// Module is disabled
255    Disabled,
256}
257
258/// Module manager for installing, updating, and managing modules
259pub struct ModuleManager {
260    /// Installed modules indexed by ID
261    installed: HashMap<String, Module>,
262    /// Module status tracking
263    status: HashMap<String, ModuleStatus>,
264    /// Base path for module storage
265    registry_path: PathBuf,
266    /// Namespace isolation enabled
267    isolation_enabled: bool,
268}
269
270impl ModuleManager {
271    /// Create a new module manager
272    pub fn new(registry_path: impl Into<PathBuf>) -> Self {
273        Self {
274            installed: HashMap::new(),
275            status: HashMap::new(),
276            registry_path: registry_path.into(),
277            isolation_enabled: true,
278        }
279    }
280
281    /// Load installed modules from the registry path
282    pub fn load(&mut self) -> Result<()> {
283        let modules_dir = self.registry_path.join("installed");
284        if !modules_dir.exists() {
285            return Ok(());
286        }
287
288        for entry in std::fs::read_dir(&modules_dir)? {
289            let entry = entry?;
290            let path = entry.path();
291            if path.is_dir() {
292                let manifest_path = path.join("module.dx");
293                if manifest_path.exists() {
294                    match self.load_manifest(&manifest_path) {
295                        Ok(mut module) => {
296                            module.install_path = Some(path.clone());
297                            self.status
298                                .insert(module.id.clone(), ModuleStatus::Installed);
299                            self.installed.insert(module.id.clone(), module);
300                        }
301                        Err(e) => {
302                            eprintln!("Warning: Failed to load module at {:?}: {}", path, e);
303                        }
304                    }
305                }
306            }
307        }
308
309        Ok(())
310    }
311
312    /// Install a module from a path
313    pub fn install(&mut self, module_path: &Path) -> Result<()> {
314        // Load the module manifest
315        let manifest_path = module_path.join("module.dx");
316        if !manifest_path.exists() {
317            return Err(DrivenError::Config(format!(
318                "Module manifest not found at {:?}",
319                manifest_path
320            )));
321        }
322
323        let module = self.load_manifest(&manifest_path)?;
324
325        // Check if already installed
326        if self.installed.contains_key(&module.id) {
327            return Err(DrivenError::Config(format!(
328                "Module '{}' is already installed",
329                module.id
330            )));
331        }
332
333        // Resolve dependencies first
334        self.resolve_dependencies(&module)?;
335
336        // Mark as installing
337        self.status
338            .insert(module.id.clone(), ModuleStatus::Installing);
339
340        // Create installation directory
341        let install_dir = self.registry_path.join("installed").join(&module.id);
342        std::fs::create_dir_all(&install_dir)?;
343
344        // Copy module files
345        self.copy_module_files(module_path, &install_dir)?;
346
347        // Update module with install path
348        let mut installed_module = module.clone();
349        installed_module.install_path = Some(install_dir);
350
351        // Register the module
352        self.status
353            .insert(installed_module.id.clone(), ModuleStatus::Installed);
354        self.installed
355            .insert(installed_module.id.clone(), installed_module);
356
357        Ok(())
358    }
359
360    /// Uninstall a module
361    pub fn uninstall(&mut self, module_id: &str) -> Result<()> {
362        let module = self.installed.get(module_id).ok_or_else(|| {
363            DrivenError::Config(format!("Module '{}' is not installed", module_id))
364        })?;
365
366        // Check if other modules depend on this one
367        for (id, other) in &self.installed {
368            if id != module_id {
369                for dep in &other.dependencies {
370                    if dep.module_id == module_id && !dep.optional {
371                        return Err(DrivenError::Config(format!(
372                            "Cannot uninstall '{}': module '{}' depends on it",
373                            module_id, id
374                        )));
375                    }
376                }
377            }
378        }
379
380        // Remove installation directory
381        if let Some(install_path) = &module.install_path {
382            if install_path.exists() {
383                std::fs::remove_dir_all(install_path)?;
384            }
385        }
386
387        // Remove from registry
388        self.installed.remove(module_id);
389        self.status.remove(module_id);
390
391        Ok(())
392    }
393
394    /// Update a module to a new version
395    pub fn update(&mut self, module_id: &str, new_module_path: &Path) -> Result<()> {
396        // Verify the module is installed
397        if !self.installed.contains_key(module_id) {
398            return Err(DrivenError::Config(format!(
399                "Module '{}' is not installed",
400                module_id
401            )));
402        }
403
404        // Load new module manifest
405        let manifest_path = new_module_path.join("module.dx");
406        let new_module = self.load_manifest(&manifest_path)?;
407
408        // Verify it's the same module
409        if new_module.id != module_id {
410            return Err(DrivenError::Config(format!(
411                "Module ID mismatch: expected '{}', got '{}'",
412                module_id, new_module.id
413            )));
414        }
415
416        // Uninstall old version
417        self.uninstall(module_id)?;
418
419        // Install new version
420        self.install(new_module_path)?;
421
422        Ok(())
423    }
424
425    /// List all installed modules
426    pub fn list_installed(&self) -> Vec<&Module> {
427        self.installed.values().collect()
428    }
429
430    /// Get a specific installed module
431    pub fn get(&self, module_id: &str) -> Option<&Module> {
432        self.installed.get(module_id)
433    }
434
435    /// Get module status
436    pub fn get_status(&self, module_id: &str) -> Option<ModuleStatus> {
437        self.status.get(module_id).copied()
438    }
439
440    /// Resolve dependencies for a module
441    pub fn resolve_dependencies(&self, module: &Module) -> Result<Vec<&Module>> {
442        let mut resolved = Vec::new();
443        let mut to_resolve: Vec<&ModuleDependency> = module.dependencies.iter().collect();
444        let mut visited = std::collections::HashSet::new();
445
446        while let Some(dep) = to_resolve.pop() {
447            if visited.contains(&dep.module_id) {
448                continue;
449            }
450            visited.insert(dep.module_id.clone());
451
452            match self.installed.get(&dep.module_id) {
453                Some(installed) => {
454                    if !installed.satisfies_version(&dep.version_req) {
455                        return Err(DrivenError::Config(format!(
456                            "Dependency '{}' version {} does not satisfy requirement {}",
457                            dep.module_id, installed.version, dep.version_req
458                        )));
459                    }
460                    resolved.push(installed);
461                    // Add transitive dependencies
462                    for trans_dep in &installed.dependencies {
463                        to_resolve.push(trans_dep);
464                    }
465                }
466                None if !dep.optional => {
467                    return Err(DrivenError::Config(format!(
468                        "Required dependency '{}' ({}) is not installed",
469                        dep.module_id, dep.version_req
470                    )));
471                }
472                None => {
473                    // Optional dependency not installed, skip
474                }
475            }
476        }
477
478        Ok(resolved)
479    }
480
481    /// Check if a module is installed
482    pub fn is_installed(&self, module_id: &str) -> bool {
483        self.installed.contains_key(module_id)
484    }
485
486    /// Enable namespace isolation
487    pub fn enable_isolation(&mut self) {
488        self.isolation_enabled = true;
489    }
490
491    /// Disable namespace isolation
492    pub fn disable_isolation(&mut self) {
493        self.isolation_enabled = false;
494    }
495
496    /// Check if isolation is enabled
497    pub fn is_isolation_enabled(&self) -> bool {
498        self.isolation_enabled
499    }
500
501    /// Get all agents from installed modules (with namespacing if enabled)
502    pub fn get_all_agents(&self) -> Vec<String> {
503        let mut agents = Vec::new();
504        for module in self.installed.values() {
505            for agent in &module.agents {
506                if self.isolation_enabled {
507                    agents.push(module.namespaced_agent(agent));
508                } else {
509                    agents.push(agent.clone());
510                }
511            }
512        }
513        agents
514    }
515
516    /// Get all workflows from installed modules (with namespacing if enabled)
517    pub fn get_all_workflows(&self) -> Vec<String> {
518        let mut workflows = Vec::new();
519        for module in self.installed.values() {
520            for workflow in &module.workflows {
521                if self.isolation_enabled {
522                    workflows.push(module.namespaced_workflow(workflow));
523                } else {
524                    workflows.push(workflow.clone());
525                }
526            }
527        }
528        workflows
529    }
530
531    /// Get all templates from installed modules (with namespacing if enabled)
532    pub fn get_all_templates(&self) -> Vec<String> {
533        let mut templates = Vec::new();
534        for module in self.installed.values() {
535            for template in &module.templates {
536                if self.isolation_enabled {
537                    templates.push(module.namespaced_template(template));
538                } else {
539                    templates.push(template.clone());
540                }
541            }
542        }
543        templates
544    }
545
546    // Private helper methods
547
548    fn load_manifest(&self, path: &Path) -> Result<Module> {
549        let content = std::fs::read_to_string(path)?;
550        self.parse_dx_manifest(&content)
551    }
552
553    fn parse_dx_manifest(&self, content: &str) -> Result<Module> {
554        // Parse DX LLM format manifest
555        let mut module = Module::new("", "", "");
556
557        for line in content.lines() {
558            let line = line.trim();
559            if line.is_empty() || line.starts_with('#') {
560                continue;
561            }
562
563            if let Some((key, value)) = line.split_once('|') {
564                let key = key.trim();
565                let value = value.trim();
566
567                match key {
568                    "id" => module.id = value.to_string(),
569                    "nm" | "name" => module.name = value.to_string(),
570                    "v" | "version" => module.version = value.to_string(),
571                    "desc" | "description" => module.description = value.to_string(),
572                    "author" => module.author = Some(value.to_string()),
573                    "license" => module.license = Some(value.to_string()),
574                    key if key.starts_with("dep.") => {
575                        let dep_id = key.strip_prefix("dep.").unwrap();
576                        module
577                            .dependencies
578                            .push(ModuleDependency::new(dep_id, value));
579                    }
580                    key if key.starts_with("agent.") => {
581                        module.agents.push(value.to_string());
582                    }
583                    key if key.starts_with("workflow.") => {
584                        module.workflows.push(value.to_string());
585                    }
586                    key if key.starts_with("template.") => {
587                        module.templates.push(value.to_string());
588                    }
589                    key if key.starts_with("resource.") => {
590                        module.resources.push(value.to_string());
591                    }
592                    _ => {}
593                }
594            }
595        }
596
597        if module.id.is_empty() {
598            return Err(DrivenError::Config(
599                "Module manifest missing 'id' field".to_string(),
600            ));
601        }
602        if module.name.is_empty() {
603            module.name = module.id.clone();
604        }
605        if module.version.is_empty() {
606            module.version = "0.0.0".to_string();
607        }
608
609        Ok(module)
610    }
611
612    fn copy_module_files(&self, src: &Path, dest: &Path) -> Result<()> {
613        // Copy all files from source to destination
614        for entry in std::fs::read_dir(src)? {
615            let entry = entry?;
616            let src_path = entry.path();
617            let dest_path = dest.join(entry.file_name());
618
619            if src_path.is_dir() {
620                std::fs::create_dir_all(&dest_path)?;
621                self.copy_module_files(&src_path, &dest_path)?;
622            } else {
623                std::fs::copy(&src_path, &dest_path)?;
624            }
625        }
626        Ok(())
627    }
628}
629
630impl Default for ModuleManager {
631    fn default() -> Self {
632        Self::new(".driven/modules")
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639
640    #[test]
641    fn test_module_creation() {
642        let module = Module::new("test-module", "Test Module", "1.0.0")
643            .with_description("A test module")
644            .with_agent("test-agent")
645            .with_workflow("test-workflow");
646
647        assert_eq!(module.id, "test-module");
648        assert_eq!(module.name, "Test Module");
649        assert_eq!(module.version, "1.0.0");
650        assert_eq!(module.agents.len(), 1);
651        assert_eq!(module.workflows.len(), 1);
652    }
653
654    #[test]
655    fn test_version_satisfaction() {
656        let module = Module::new("test", "Test", "1.2.3");
657
658        assert!(module.satisfies_version("1.2.3"));
659        assert!(module.satisfies_version("*"));
660        assert!(module.satisfies_version("^1"));
661        assert!(module.satisfies_version("~1.2"));
662        assert!(!module.satisfies_version("2.0.0"));
663        assert!(!module.satisfies_version("^2"));
664    }
665
666    #[test]
667    fn test_namespacing() {
668        let module = Module::new("my-module", "My Module", "1.0.0")
669            .with_agent("agent1")
670            .with_workflow("workflow1");
671
672        assert_eq!(module.namespaced_agent("agent1"), "my-module:agent1");
673        assert_eq!(
674            module.namespaced_workflow("workflow1"),
675            "my-module:workflow1"
676        );
677    }
678
679    #[test]
680    fn test_dependency_creation() {
681        let dep = ModuleDependency::new("other-module", "^1.0.0");
682        assert_eq!(dep.module_id, "other-module");
683        assert_eq!(dep.version_req, "^1.0.0");
684        assert!(!dep.optional);
685
686        let opt_dep = ModuleDependency::optional("optional-module", "*");
687        assert!(opt_dep.optional);
688    }
689
690    #[test]
691    fn test_module_manager_creation() {
692        let manager = ModuleManager::new("/tmp/test-modules");
693        assert!(manager.list_installed().is_empty());
694        assert!(manager.is_isolation_enabled());
695    }
696}