rpi_tools/tools/
powershell.rs1use std::sync::Arc;
6
7use async_trait::async_trait;
8use base64::{engine::general_purpose::STANDARD, Engine as _};
9use rpi_agent::agent_tool::AgentTool;
10use rpi_agent::error::AgentError;
11use rpi_agent::types::{AgentToolResult, TextContentOrImage, ToolResultPartial};
12use rpi_ai::types::Tool;
13use schemars::JsonSchema;
14use serde::Deserialize;
15use tokio_util::sync::CancellationToken;
16
17use crate::shell_output::{execute_shell_with_capture, ShellCaptureOptions};
18use crate::tools::tool_context::ExecutionToolContext;
19
20#[derive(Debug, Clone, JsonSchema, Deserialize)]
21pub struct PowerShellInput {
22 pub command: String,
23 #[serde(default)]
24 pub timeout: Option<f64>,
25}
26
27pub struct PowerShellTool {
28 schema: Tool,
29 env: Arc<dyn crate::env::ExecutionEnv>,
30}
31
32impl PowerShellTool {
33 fn schema() -> Tool {
34 Tool {
35 name: "powershell".into(),
36 description: "Execute a PowerShell command on Windows. Returns stdout and stderr."
37 .into(),
38 parameters: rpi_ai::types::Schema::new(
39 serde_json::to_value(schemars::schema_for!(PowerShellInput)).unwrap_or_default(),
40 ),
41 constrained_sampling: None,
42 }
43 }
44}
45
46pub fn create_powershell_tool(context: &ExecutionToolContext) -> Arc<dyn AgentTool> {
47 Arc::new(PowerShellTool {
48 schema: PowerShellTool::schema(),
49 env: context.env().clone(),
50 })
51}
52
53#[async_trait]
54impl AgentTool for PowerShellTool {
55 fn schema(&self) -> &Tool {
56 &self.schema
57 }
58 fn label(&self) -> &str {
59 "powershell"
60 }
61
62 async fn execute(
63 &self,
64 _tool_call_id: &str,
65 params: serde_json::Value,
66 signal: CancellationToken,
67 _on_update: Arc<dyn Fn(ToolResultPartial) + Send + Sync>,
68 ) -> Result<AgentToolResult, AgentError> {
69 if !cfg!(windows) {
70 return Err(AgentError::Tool(
71 "PowerShell tool is only available on Windows".into(),
72 ));
73 }
74 let input: PowerShellInput = serde_json::from_value(params)
75 .map_err(|e| AgentError::Validation(format!("powershell input invalid: {e}")))?;
76 let utf16: Vec<u8> = input
77 .command
78 .encode_utf16()
79 .flat_map(u16::to_le_bytes)
80 .collect();
81 let encoded = STANDARD.encode(utf16);
82 let command =
83 format!("powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand {encoded}");
84 let capture = execute_shell_with_capture(
85 &self.env,
86 &command,
87 ShellCaptureOptions {
88 cwd: Some(self.env.cwd().to_path_buf()),
89 env: None,
90 inherit_env: true,
91 timeout: input.timeout,
92 cancel: Some(&signal),
93 on_chunk: None,
94 return_execution_errors: true,
95 },
96 )
97 .await
98 .map_err(|e| AgentError::Tool(e.to_string()))?;
99 let mut output = capture.output;
100 if capture.cancelled {
101 return Err(AgentError::Tool("PowerShell command aborted".into()));
102 }
103 if let Some(error) = capture.execution_error {
104 return Err(AgentError::Tool(error.to_string()));
105 }
106 if let Some(code) = capture.exit_code.filter(|code| *code != 0) {
107 output.push_str(&format!("\n\nPowerShell exited with code {code}"));
108 return Err(AgentError::Tool(output));
109 }
110 if output.trim().is_empty() {
111 output = "(no output)".into();
112 }
113 Ok(AgentToolResult {
114 content: vec![TextContentOrImage::text(output)],
115 ..Default::default()
116 })
117 }
118}