ai_agent/tools/powershell/
powershell_tool.rs1use crate::error::AgentError;
5use crate::tools::powershell::prompt::get_default_timeout_ms;
6use crate::tools::powershell::powershell_security::powershell_command_is_safe;
7use crate::tools::powershell::tool_name::POWERSHELL_TOOL_NAME;
8use crate::types::*;
9
10pub struct PowerShellTool;
12
13impl PowerShellTool {
14 pub fn new() -> Self {
15 Self
16 }
17
18 pub fn name(&self) -> &str {
19 POWERSHELL_TOOL_NAME
20 }
21
22 pub fn description(&self) -> String {
23 "Execute a PowerShell command. Windows-only tool for PowerShell cmdlets and native executable execution"
24 .to_string()
25 }
26
27 pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
28 "PowerShell".to_string()
29 }
30
31 pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
32 input.and_then(|inp| inp["command"].as_str().map(String::from))
33 }
34
35 pub fn render_tool_result_message(
36 &self,
37 content: &serde_json::Value,
38 ) -> Option<String> {
39 let content_str = content["content"].as_str()?;
40 if content_str.is_empty() {
41 Some("No output".to_string())
42 } else {
43 let line_count = content_str.lines().count();
44 Some(format!(
45 "{} {}",
46 line_count,
47 if line_count == 1 { "line" } else { "lines" }
48 ))
49 }
50 }
51
52 pub fn input_schema(&self) -> ToolInputSchema {
53 ToolInputSchema {
54 schema_type: "object".to_string(),
55 properties: serde_json::json!({
56 "command": {
57 "type": "string",
58 "description": "PowerShell command to execute"
59 },
60 "timeout": {
61 "type": "number",
62 "description": "Optional timeout in milliseconds (default: 120000, max: 600000)"
63 },
64 "description": {
65 "type": "string",
66 "description": "Brief description of what this command does"
67 },
68 "run_in_background": {
69 "type": "boolean",
70 "description": "Run the command in the background (default: false)"
71 }
72 }),
73 required: Some(vec!["command".to_string()]),
74 }
75 }
76
77 pub async fn execute(
78 &self,
79 input: serde_json::Value,
80 context: &ToolContext,
81 ) -> Result<ToolResult, AgentError> {
82 let command = input["command"]
83 .as_str()
84 .ok_or_else(|| AgentError::Tool("command is required".to_string()))?
85 .to_string();
86
87 let security = powershell_command_is_safe(&command);
89 if security.behavior
90 == crate::tools::powershell::powershell_security::SecurityBehavior::Ask
91 {
92 let _warning = security.message;
94 }
95
96 let cwd = context.cwd.clone();
97 let output = tokio::task::spawn_blocking(move || {
98 let mut cmd = if let Ok(output) = std::process::Command::new("pwsh")
100 .arg("-c")
101 .arg("--version")
102 .output()
103 {
104 if output.status.success() {
105 let mut c = std::process::Command::new("pwsh");
106 c.arg("-NoProfile").arg("-Command").arg(&command);
107 c
108 } else {
109 let mut c = std::process::Command::new("powershell");
110 c.arg("-NoProfile")
111 .arg("-NonInteractive")
112 .arg("-Command")
113 .arg(&command);
114 c
115 }
116 } else {
117 let mut c = std::process::Command::new("powershell");
118 c.arg("-NoProfile")
119 .arg("-NonInteractive")
120 .arg("-Command")
121 .arg(&command);
122 c
123 };
124
125 if !cwd.is_empty() {
126 cmd.current_dir(&cwd);
127 }
128 cmd.output()
129 })
130 .await
131 .map_err(|e| AgentError::Tool(e.to_string()))?
132 .map_err(|e| {
133 AgentError::Tool(format!(
134 "Failed to execute PowerShell command: {}. Make sure PowerShell is installed.",
135 e
136 ))
137 })?;
138
139 let stdout = String::from_utf8_lossy(&output.stdout);
140 let stderr = String::from_utf8_lossy(&output.stderr);
141
142 let content = if !stdout.trim().is_empty() {
143 stdout.to_string()
144 } else if !stderr.trim().is_empty() {
145 stderr.to_string()
146 } else {
147 "".to_string()
148 };
149
150 let is_error = !output.status.success();
151
152 Ok(ToolResult {
153 result_type: "tool_result".to_string(),
154 tool_use_id: "".to_string(),
155 content,
156 is_error: Some(is_error),
157 was_persisted: None,
158 })
159 }
160}
161
162impl Default for PowerShellTool {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_powershell_tool_name() {
174 let tool = PowerShellTool::new();
175 assert_eq!(tool.name(), POWERSHELL_TOOL_NAME);
176 }
177
178 #[test]
179 fn test_powershell_tool_schema() {
180 let tool = PowerShellTool::new();
181 let schema = tool.input_schema();
182 assert_eq!(schema.schema_type, "object");
183 assert!(schema.properties["command"].is_object());
184 assert!(schema.required.is_some());
185 }
186
187 #[test]
188 fn test_powershell_tool_user_facing_name() {
189 let tool = PowerShellTool::new();
190 assert_eq!(tool.user_facing_name(None), "PowerShell");
191 }
192
193 #[test]
194 fn test_powershell_tool_summary() {
195 let tool = PowerShellTool::new();
196 let input = serde_json::json!({"command": "Get-ChildItem"});
197 let summary = tool.get_tool_use_summary(Some(&input));
198 assert_eq!(summary, Some("Get-ChildItem".to_string()));
199 }
200
201 #[test]
202 fn test_powershell_tool_description_not_empty() {
203 let tool = PowerShellTool::new();
204 assert!(!tool.description().is_empty());
205 }
206}