Skip to main content

telos_agent/integrations/plugin/registry/
discovery.rs

1//! Plugin discovery from installed directories.
2
3use crate::integrations::plugin::manifest::PluginManifest;
4use crate::integrations::plugin::registry::lifecycle::PluginRegistry;
5use crate::integrations::plugin::registry::types::LoadedPlugin;
6use crate::integrations::plugin::{PluginError, PluginId, PluginSource};
7use std::path::{Path, PathBuf};
8
9impl PluginRegistry {
10    /// Scan the installed directory and load all plugins found there.
11    ///
12    /// Each subdirectory that contains a `plugin.json` is loaded.
13    /// Plugins are NOT auto-enabled — state is restored from `plugin_state.json`.
14    pub fn discover_installed(&mut self) -> Result<Vec<PluginId>, PluginError> {
15        let installed_dir = self.installed_dir();
16        if !installed_dir.exists() {
17            return Ok(Vec::new());
18        }
19
20        let mut discovered = Vec::new();
21        let entries = std::fs::read_dir(&installed_dir)?;
22
23        for entry in entries {
24            let entry = entry?;
25            let dir = entry.path();
26            if !dir.is_dir() {
27                continue;
28            }
29
30            let manifest_path = dir.join("plugin.json");
31            if !manifest_path.exists() {
32                continue;
33            }
34
35            match self.load_plugin_from_dir(&dir) {
36                Ok(plugin) => {
37                    let id = plugin.id.clone();
38                    discovered.push(id.clone());
39                    self.register(plugin);
40                }
41                Err(err) => {
42                    tracing::warn!(
43                        dir = %dir.display(),
44                        error = %err,
45                        "failed to load plugin from installed directory"
46                    );
47                }
48            }
49        }
50
51        Ok(discovered)
52    }
53    /// Load a single plugin from a directory containing plugin.json.
54    fn load_plugin_from_dir(&self, dir: &Path) -> Result<LoadedPlugin, PluginError> {
55        let manifest_path = dir.join("plugin.json");
56        let content =
57            std::fs::read_to_string(&manifest_path).map_err(|e| PluginError::ManifestParse {
58                path: manifest_path.clone(),
59                reason: format!("failed to read: {e}"),
60            })?;
61
62        let raw: serde_json::Value =
63            serde_json::from_str(&content).map_err(|e| PluginError::ManifestParse {
64                path: manifest_path.clone(),
65                reason: format!("invalid JSON: {e}"),
66            })?;
67        if raw.get("hooks").is_some() || raw.get("interceptors").is_some() {
68            return Err(PluginError::ManifestValidation {
69                errors: vec![
70                    "the `hooks` and `interceptors` plugin fields were removed; use `policies`"
71                        .into(),
72                ],
73            });
74        }
75        let manifest: PluginManifest =
76            serde_json::from_value(raw).map_err(|e| PluginError::ManifestParse {
77                path: manifest_path.clone(),
78                reason: format!("invalid manifest: {e}"),
79            })?;
80
81        if manifest.name.is_empty() {
82            return Err(PluginError::ManifestValidation {
83                errors: vec!["name must not be empty".into()],
84            });
85        }
86
87        // Determine marketplace from the parent directory name
88        // installed/<name>@<marketplace>/plugin.json
89        let dir_name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("unknown@unknown");
90        let id = PluginId::parse(dir_name).unwrap_or_else(|| PluginId {
91            name: manifest.name.clone(),
92            marketplace: "unknown".into(),
93        });
94
95        // Resolve component paths
96        let resolve = |paths: &Option<Vec<String>>| -> Vec<PathBuf> {
97            paths.as_ref().map(|p| p.iter().map(|s| dir.join(s)).collect()).unwrap_or_default()
98        };
99
100        // Find .json tool files in the tools/ directory
101        let mut resolved_tools = resolve(&manifest.tools);
102        let tools_dir = dir.join("tools");
103        if tools_dir.exists()
104            && tools_dir.is_dir()
105            && let Ok(entries) = std::fs::read_dir(&tools_dir)
106        {
107            for entry in entries.flatten() {
108                let p = entry.path();
109                if p.extension().is_some_and(|ext| ext == "json") {
110                    resolved_tools.push(p);
111                }
112            }
113        }
114        resolved_tools.sort();
115        resolved_tools.dedup();
116
117        let resolved_skills = resolve(&manifest.skills);
118        let resolved_agents = resolve(&manifest.agents);
119        let resolved_prompt_sections = resolve(&manifest.prompt_sections);
120        let resolved_output_styles = resolve(&manifest.output_styles);
121
122        Ok(LoadedPlugin {
123            id,
124            manifest,
125            path: dir.to_path_buf(),
126            source: PluginSource::Local { path: dir.to_path_buf() },
127            enabled: false, // will be set by state restoration
128            is_builtin: false,
129            resolved_tools,
130            resolved_skills,
131            resolved_agents,
132            resolved_prompt_sections,
133            resolved_output_styles,
134        })
135    }
136}