cersei_tools/
powershell.rs1use 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}