Skip to main content

ai_agent/tools/powershell/
powershell_tool.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/tools/PowerShellTool/PowerShellTool.tsx
2//! PowerShell tool - executes PowerShell commands
3
4use 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
10/// PowerShell tool - executes PowerShell commands with security checks
11pub 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        // Security check
88        let security = powershell_command_is_safe(&command);
89        if security.behavior
90            == crate::tools::powershell::powershell_security::SecurityBehavior::Ask
91        {
92            // For now, pass through with warning (full permission handling done at agent level)
93            let _warning = security.message;
94        }
95
96        let cwd = context.cwd.clone();
97        let output = tokio::task::spawn_blocking(move || {
98            // Try pwsh first (PowerShell 7+), fall back to powershell.exe
99            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}