telos_agent/integrations/plugin/registry/
discovery.rs1use 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 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 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 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 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 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, is_builtin: false,
129 resolved_tools,
130 resolved_skills,
131 resolved_agents,
132 resolved_prompt_sections,
133 resolved_output_styles,
134 })
135 }
136}