Skip to main content

cersei_tools/
powershell.rs

1//! PowerShell tool: execute PowerShell commands (Windows/cross-platform).
2
3use super::*;
4use serde::Deserialize;
5use std::process::Stdio;
6
7pub struct PowerShellTool;
8
9#[async_trait]
10impl Tool for PowerShellTool {
11    fn name(&self) -> &str {
12        "PowerShell"
13    }
14    fn description(&self) -> &str {
15        "Execute a PowerShell command. Available on Windows; on macOS/Linux uses pwsh if installed."
16    }
17    fn permission_level(&self) -> PermissionLevel {
18        PermissionLevel::Execute
19    }
20    fn category(&self) -> ToolCategory {
21        ToolCategory::Shell
22    }
23
24    fn input_schema(&self) -> Value {
25        serde_json::json!({
26            "type": "object",
27            "properties": {
28                "command": { "type": "string", "description": "PowerShell command to execute" },
29                "timeout": { "type": "integer", "description": "Timeout in milliseconds (default 120000)" }
30            },
31            "required": ["command"]
32        })
33    }
34
35    async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult {
36        #[derive(Deserialize)]
37        struct Input {
38            command: String,
39            timeout: Option<u64>,
40        }
41
42        let input: Input = match serde_json::from_value(input) {
43            Ok(i) => i,
44            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
45        };
46
47        let ps = if cfg!(windows) { "powershell" } else { "pwsh" };
48        if !cfg!(windows) && which::which(ps).is_err() {
49            return ToolResult::error(
50                "PowerShell (pwsh) not found. Install with: brew install powershell",
51            );
52        }
53
54        let timeout_ms = input.timeout.unwrap_or(120_000).min(600_000);
55
56        let result = tokio::time::timeout(
57            std::time::Duration::from_millis(timeout_ms),
58            tokio::process::Command::new(ps)
59                .args(["-NoProfile", "-NonInteractive", "-Command", &input.command])
60                .current_dir(&ctx.working_dir)
61                .stdin(Stdio::null())
62                .stdout(Stdio::piped())
63                .stderr(Stdio::piped())
64                .output(),
65        )
66        .await;
67
68        match result {
69            Ok(Ok(output)) => {
70                let stdout = String::from_utf8_lossy(&output.stdout);
71                let stderr = String::from_utf8_lossy(&output.stderr);
72                let mut content = String::new();
73                if !stdout.is_empty() {
74                    content.push_str(&stdout);
75                }
76                if !stderr.is_empty() {
77                    if !content.is_empty() {
78                        content.push('\n');
79                    }
80                    content.push_str(&stderr);
81                }
82                if output.status.success() {
83                    ToolResult::success(if content.is_empty() {
84                        "(no output)".into()
85                    } else {
86                        content
87                    })
88                } else {
89                    ToolResult::error(format!(
90                        "Exit code {}\n{}",
91                        output.status.code().unwrap_or(-1),
92                        content
93                    ))
94                }
95            }
96            Ok(Err(e)) => ToolResult::error(format!("Failed to execute: {}", e)),
97            Err(_) => ToolResult::error(format!("Timed out after {}ms", timeout_ms)),
98        }
99    }
100}