roboticus_server/
plugins.rs1use std::sync::Arc;
2
3use tracing::{debug, info, warn};
4
5use roboticus_core::config::PluginsConfig;
6use roboticus_plugin_sdk::loader::discover_plugins;
7use roboticus_plugin_sdk::registry::{PermissionPolicy, PluginRegistry};
8use roboticus_plugin_sdk::script::ScriptPlugin;
9
10pub async fn init_plugin_registry(config: &PluginsConfig) -> Arc<PluginRegistry> {
12 let registry = Arc::new(PluginRegistry::new(
13 config.allow.clone(),
14 config.deny.clone(),
15 PermissionPolicy {
16 strict: config.strict_permissions,
17 allowed: config.allowed_permissions.clone(),
18 },
19 ));
20
21 let plugins_dir = &config.dir;
22 if !plugins_dir.exists() {
23 debug!(dir = %plugins_dir.display(), "plugins directory does not exist, skipping discovery");
24 return registry;
25 }
26
27 let discovered = match discover_plugins(plugins_dir) {
28 Ok(d) => d,
29 Err(e) => {
30 warn!(error = %e, "failed to discover plugins");
31 return registry;
32 }
33 };
34
35 info!(count = discovered.len(), "discovered plugins");
36
37 for dp in discovered {
38 let name = dp.manifest.name.clone();
39 let version = dp.manifest.version.clone();
40 let tool_count = dp.manifest.tools.len();
41
42 let report = dp.manifest.vet(&dp.dir);
44 for w in &report.warnings {
45 warn!(name = %name, warning = %w, "plugin vet warning");
46 }
47 if !report.is_ok() {
48 for e in &report.errors {
49 warn!(name = %name, error = %e, "plugin vet error");
50 }
51 warn!(
52 name = %name,
53 errors = report.errors.len(),
54 "skipping plugin due to vet errors"
55 );
56 continue;
57 }
58
59 let timeout_secs = dp.manifest.timeout_seconds;
60 let mut plugin = ScriptPlugin::new(dp.manifest, dp.dir);
61 if let Some(secs) = timeout_secs {
62 plugin = plugin.with_timeout(std::time::Duration::from_secs(secs));
63 }
64 match registry.register(Box::new(plugin)).await {
65 Ok(()) => {
66 info!(
67 name = %name,
68 version = %version,
69 tools = tool_count,
70 "registered script plugin"
71 );
72 }
73 Err(e) => {
74 warn!(name = %name, error = %e, "failed to register plugin");
75 }
76 }
77 }
78
79 let init_errors = registry.init_all().await;
80 if !init_errors.is_empty() {
81 for err in &init_errors {
82 warn!(error = %err, "plugin init error");
83 }
84 }
85
86 let count = registry.plugin_count().await;
87 info!(active = count, "plugin registry ready");
88
89 registry
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use std::fs;
96 use std::path::PathBuf;
97
98 #[tokio::test]
99 async fn init_with_nonexistent_dir() {
100 let config = PluginsConfig {
101 dir: PathBuf::from("/nonexistent/plugins"),
102 allow: vec![],
103 deny: vec![],
104 strict_permissions: false,
105 allowed_permissions: vec![],
106 };
107 let registry = init_plugin_registry(&config).await;
108 assert_eq!(registry.plugin_count().await, 0);
109 }
110
111 #[tokio::test]
112 async fn init_with_empty_dir() {
113 let dir = tempfile::tempdir().unwrap();
114 let config = PluginsConfig {
115 dir: dir.path().to_path_buf(),
116 allow: vec![],
117 deny: vec![],
118 strict_permissions: false,
119 allowed_permissions: vec![],
120 };
121 let registry = init_plugin_registry(&config).await;
122 assert_eq!(registry.plugin_count().await, 0);
123 }
124
125 #[tokio::test]
126 async fn init_discovers_and_registers_plugin() {
127 let dir = tempfile::tempdir().unwrap();
128 let plugin_dir = dir.path().join("hello-plugin");
129 fs::create_dir(&plugin_dir).unwrap();
130 fs::write(
131 plugin_dir.join("plugin.toml"),
132 r#"
133name = "hello-plugin"
134version = "0.1.0"
135description = "A test plugin"
136
137[[tools]]
138name = "say_hello"
139description = "Says hello"
140"#,
141 )
142 .unwrap();
143 fs::write(plugin_dir.join("say_hello.gosh"), "echo hello").unwrap();
144
145 let config = PluginsConfig {
146 dir: dir.path().to_path_buf(),
147 allow: vec![],
148 deny: vec![],
149 strict_permissions: false,
150 allowed_permissions: vec![],
151 };
152 let registry = init_plugin_registry(&config).await;
153 assert_eq!(registry.plugin_count().await, 1);
154
155 let plugins = registry.list_plugins().await;
156 assert_eq!(plugins[0].name, "hello-plugin");
157 assert_eq!(plugins[0].tools.len(), 1);
158 }
159
160 #[tokio::test]
161 async fn deny_list_blocks_plugin() {
162 let dir = tempfile::tempdir().unwrap();
163 let plugin_dir = dir.path().join("blocked");
164 fs::create_dir(&plugin_dir).unwrap();
165 fs::write(
166 plugin_dir.join("plugin.toml"),
167 "name = \"blocked\"\nversion = \"1.0.0\"\n",
168 )
169 .unwrap();
170
171 let config = PluginsConfig {
172 dir: dir.path().to_path_buf(),
173 allow: vec![],
174 deny: vec!["blocked".into()],
175 strict_permissions: false,
176 allowed_permissions: vec![],
177 };
178 let registry = init_plugin_registry(&config).await;
179 assert_eq!(registry.plugin_count().await, 0);
180 }
181
182 #[tokio::test]
183 async fn init_respects_timeout_seconds() {
184 let dir = tempfile::tempdir().unwrap();
185 let plugin_dir = dir.path().join("slow-plugin");
186 fs::create_dir(&plugin_dir).unwrap();
187 fs::write(
188 plugin_dir.join("plugin.toml"),
189 r#"
190name = "slow-plugin"
191version = "0.1.0"
192timeout_seconds = 300
193
194[[tools]]
195name = "slow_task"
196description = "A long-running task"
197"#,
198 )
199 .unwrap();
200 fs::write(plugin_dir.join("slow_task.sh"), "#!/bin/sh\necho done").unwrap();
201
202 let config = PluginsConfig {
203 dir: dir.path().to_path_buf(),
204 allow: vec![],
205 deny: vec![],
206 strict_permissions: false,
207 allowed_permissions: vec![],
208 };
209 let registry = init_plugin_registry(&config).await;
210 assert_eq!(registry.plugin_count().await, 1);
211 let result = registry
213 .execute_tool("slow_task", &serde_json::json!({}))
214 .await
215 .unwrap();
216 assert!(result.success);
217 }
218
219 #[tokio::test]
220 async fn plugin_tool_execution() {
221 let dir = tempfile::tempdir().unwrap();
222 let plugin_dir = dir.path().join("echo-plugin");
223 fs::create_dir(&plugin_dir).unwrap();
224 fs::write(
225 plugin_dir.join("plugin.toml"),
226 r#"
227name = "echo-plugin"
228version = "0.1.0"
229[[tools]]
230name = "echo"
231description = "Echoes input"
232"#,
233 )
234 .unwrap();
235 fs::write(
236 plugin_dir.join("echo.sh"),
237 "#!/bin/sh\necho $ROBOTICUS_INPUT",
238 )
239 .unwrap();
240
241 let config = PluginsConfig {
242 dir: dir.path().to_path_buf(),
243 allow: vec![],
244 deny: vec![],
245 strict_permissions: false,
246 allowed_permissions: vec![],
247 };
248 let registry = init_plugin_registry(&config).await;
249
250 let result = registry
251 .execute_tool("echo", &serde_json::json!({"msg": "hi"}))
252 .await
253 .unwrap();
254 assert!(result.success);
255 assert!(result.output.contains("msg"));
256 }
257}