Skip to main content

agent_engine/tools/
powershell.rs

1//! `powershell` tool — Windows-native shell execution.
2//!
3//! Registered only on Windows. Wraps the shared `run_shell_command` core with
4//! the PowerShell invocation (`-NoProfile -ExecutionPolicy Bypass -Command`). This gives the model a
5//! native shell on Windows hosts: no quoting translation through bash, no
6//! WSL/Git-Bash path impedance, native cmdlets (`Get-Process`, `Get-ChildItem`),
7//! and direct access to Windows paths.
8
9use super::{bash::ShellSpec, Tool, ToolContext};
10use crate::{Result, RuntimeError};
11use serde_json::{json, Value};
12
13pub struct PowerShellTool;
14
15#[async_trait::async_trait]
16impl Tool for PowerShellTool {
17    fn origin(&self) -> crate::tools::ToolOrigin {
18        crate::tools::ToolOrigin::Builtin
19    }
20
21    /// Same effect class as bash: arbitrary shell side effects.
22    fn effect(&self) -> crate::tools::catalog::ToolEffect {
23        crate::tools::catalog::ToolEffect::NonIdempotent
24    }
25
26    fn name(&self) -> &str {
27        "powershell"
28    }
29
30    fn description(&self) -> &str {
31        "Execute a PowerShell command on the Windows host. Use this for Windows-native tasks: \
32         file system operations on C:\\ paths, registry, services, processes, Windows APIs. \
33         Runs in PowerShell 7 (pwsh) when installed, else Windows PowerShell 5.1; don't start \
34         another powershell or pwsh inside it. Scripts may run: call them directly, e.g. \
35         .\\test.ps1. PowerShell quoting rules apply — single quotes are literal, double quotes \
36         interpolate ($var, `n). Native cmdlets like Get-Process, Get-ChildItem, Test-Path are \
37         available. Commands time out after 30 seconds by default; pass a larger timeout when \
38         needed."
39    }
40
41    fn parameters(&self) -> Value {
42        json!({
43            "type": "object",
44            "properties": {
45                "command": {
46                    "type": "string",
47                    "description": "The PowerShell command to execute"
48                },
49                "timeout": {
50                    "type": "integer",
51                    "description": "Timeout in seconds (default: 30). Use a larger value for long-running commands."
52                }
53            },
54            "required": ["command"]
55        })
56    }
57
58    async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
59        let command = params["command"]
60            .as_str()
61            .ok_or_else(|| RuntimeError::Tool("Missing command parameter".to_string()))?;
62
63        super::bash::run_shell_command(
64            command,
65            ShellSpec::PowerShell,
66            params["timeout"].as_u64(),
67            ctx,
68            None,
69        )
70        .await
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn powershell_tool_schema() {
80        let tool = PowerShellTool;
81        assert_eq!(tool.name(), "powershell");
82        let params = tool.parameters();
83        assert!(params["properties"]["command"].is_object());
84        assert!(params["required"]
85            .as_array()
86            .unwrap()
87            .contains(&json!("command")));
88    }
89
90    #[cfg(windows)]
91    #[tokio::test]
92    async fn powershell_executes_simple_command() {
93        let tool = PowerShellTool;
94        let ctx = super::super::test_helpers::create_tool_context();
95        let result = tool
96            .execute(json!({ "command": "Write-Output hello-ps" }), ctx)
97            .await
98            .unwrap();
99        assert!(result.contains("hello-ps"), "got: {result}");
100    }
101}