agent_code_lib/tools/
plugin_exec.rs1use std::path::PathBuf;
14
15use async_trait::async_trait;
16use tokio::process::Command;
17use tracing::debug;
18
19use super::{Tool, ToolContext, ToolResult};
20use crate::error::ToolError;
21
22pub struct PluginExecTool {
24 tool_name: String,
26 tool_description: String,
28 binary_path: PathBuf,
30}
31
32impl PluginExecTool {
33 pub fn new(name: String, description: String, binary_path: PathBuf) -> Self {
35 Self {
36 tool_name: name,
37 tool_description: description,
38 binary_path,
39 }
40 }
41
42 fn leaked_name(&self) -> &'static str {
45 Box::leak(self.tool_name.clone().into_boxed_str())
48 }
49
50 fn leaked_description(&self) -> &'static str {
51 Box::leak(self.tool_description.clone().into_boxed_str())
52 }
53}
54
55#[async_trait]
56impl Tool for PluginExecTool {
57 fn name(&self) -> &'static str {
58 self.leaked_name()
59 }
60
61 fn description(&self) -> &'static str {
62 self.leaked_description()
63 }
64
65 fn input_schema(&self) -> serde_json::Value {
66 serde_json::json!({
67 "type": "object",
68 "properties": {
69 "input": {
70 "type": "string",
71 "description": "Input to pass to the plugin executable"
72 }
73 }
74 })
75 }
76
77 async fn call(
78 &self,
79 input: serde_json::Value,
80 _ctx: &ToolContext,
81 ) -> Result<ToolResult, ToolError> {
82 let input_str = serde_json::to_string(&input).unwrap_or_default();
83
84 debug!(
85 "Executing plugin tool '{}': {}",
86 self.tool_name,
87 self.binary_path.display()
88 );
89
90 let output = Command::new(&self.binary_path)
91 .stdin(std::process::Stdio::piped())
92 .stdout(std::process::Stdio::piped())
93 .stderr(std::process::Stdio::piped())
94 .spawn()
95 .map_err(|e| ToolError::ExecutionFailed(format!("Failed to spawn plugin: {e}")))?
96 .wait_with_output()
97 .await
98 .map_err(|e| ToolError::ExecutionFailed(format!("Plugin execution failed: {e}")))?;
99
100 let _ = input_str;
103
104 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
105 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
106
107 if output.status.success() {
108 Ok(ToolResult {
109 content: stdout,
110 is_error: false,
111 })
112 } else {
113 let error_msg = if stderr.is_empty() {
114 format!("Plugin exited with code {}", output.status)
115 } else {
116 format!(
117 "Plugin exited with code {}: {}",
118 output.status,
119 stderr.trim()
120 )
121 };
122 Ok(ToolResult {
123 content: error_msg,
124 is_error: true,
125 })
126 }
127 }
128}
129
130pub fn discover_plugin_executables(
132 plugin_path: &std::path::Path,
133 plugin_name: &str,
134) -> Vec<PluginExecTool> {
135 let bin_dir = plugin_path.join("bin");
136 if !bin_dir.is_dir() {
137 return Vec::new();
138 }
139
140 let entries = match std::fs::read_dir(&bin_dir) {
141 Ok(e) => e,
142 Err(_) => return Vec::new(),
143 };
144
145 let mut tools = Vec::new();
146
147 for entry in entries.flatten() {
148 let path = entry.path();
149 if !path.is_file() {
150 continue;
151 }
152
153 #[cfg(unix)]
155 {
156 use std::os::unix::fs::PermissionsExt;
157 if let Ok(meta) = path.metadata()
158 && meta.permissions().mode() & 0o111 == 0
159 {
160 continue; }
162 }
163 #[cfg(windows)]
164 {
165 if path.extension().and_then(|e| e.to_str()) != Some("exe") {
166 continue;
167 }
168 }
169
170 let bin_name = path
171 .file_stem()
172 .and_then(|s| s.to_str())
173 .unwrap_or("unknown");
174
175 let tool_name = format!("plugin__{plugin_name}__{bin_name}");
176 let description = format!("Plugin executable: {bin_name} (from {plugin_name})");
177
178 debug!(
179 "Discovered plugin executable: {} at {}",
180 tool_name,
181 path.display()
182 );
183
184 tools.push(PluginExecTool::new(tool_name, description, path));
185 }
186
187 tools
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_discover_empty_dir() {
196 let dir = tempfile::tempdir().unwrap();
197 let tools = discover_plugin_executables(dir.path(), "test-plugin");
198 assert!(tools.is_empty());
199 }
200
201 #[test]
202 fn test_discover_no_bin_dir() {
203 let dir = tempfile::tempdir().unwrap();
204 let tools = discover_plugin_executables(dir.path(), "test-plugin");
206 assert!(tools.is_empty());
207 }
208
209 #[cfg(unix)]
210 #[test]
211 fn test_discover_executable() {
212 use std::os::unix::fs::PermissionsExt;
213
214 let dir = tempfile::tempdir().unwrap();
215 let bin_dir = dir.path().join("bin");
216 std::fs::create_dir(&bin_dir).unwrap();
217
218 let exe_path = bin_dir.join("my-tool");
220 std::fs::write(&exe_path, "#!/bin/sh\necho ok").unwrap();
221 std::fs::set_permissions(&exe_path, std::fs::Permissions::from_mode(0o755)).unwrap();
222
223 let noexec_path = bin_dir.join("not-a-tool.txt");
225 std::fs::write(&noexec_path, "data").unwrap();
226
227 let tools = discover_plugin_executables(dir.path(), "test-plugin");
228 assert_eq!(tools.len(), 1);
229 assert_eq!(tools[0].tool_name, "plugin__test-plugin__my-tool");
230 }
231}