use async_trait::async_trait;
use car_engine::ToolExecutor;
use serde_json::{json, Value};
pub struct AutomationTools;
impl Default for AutomationTools {
fn default() -> Self {
Self::new()
}
}
impl AutomationTools {
pub fn new() -> Self {
Self
}
pub fn tool_defs(&self) -> Vec<Value> {
#[cfg(target_os = "macos")]
{
vec![json!({
"name": "run_applescript",
"description": "Run an AppleScript or JavaScript-for-Automation (JXA) script to \
control macOS and its apps — Finder, System Events, Notes, Calendar, Mail, \
Reminders, notifications, window/app control, clipboard, and anything \
scriptable. Returns the script's stdout/stderr. This drives the real host \
desktop (it cannot be sandboxed), so it requires full-access approval — a \
capability no sandboxed or text-only agent has. Prefer JXA (`language: \
\"javascript\"`), which models generate more cleanly than AppleScript.",
"parameters": {
"type": "object",
"properties": {
"script": {
"type": "string",
"description": "The AppleScript or JXA source to run."
},
"language": {
"type": "string",
"enum": ["applescript", "javascript"],
"description": "Script language (default applescript)."
}
},
"required": ["script"]
},
"mutating": true,
"tier": "full_access"
})]
}
#[cfg(target_os = "windows")]
{
vec![json!({
"name": "run_powershell",
"description": "Run a Windows PowerShell script to control Windows and its apps — \
toast notifications, clipboard (Get/Set-Clipboard), Explorer and COM app \
automation (New-Object -ComObject — Office, browsers, Shell), UI Automation, \
window/process control, registry, and anything scriptable. Returns the \
script's stdout/stderr. This drives the real host desktop (it cannot be \
sandboxed) and is distinct from the `shell` tool, which runs cmd.exe and \
cannot reach the GUI/COM automation surface — so it requires full-access \
approval, a capability no sandboxed or text-only agent has.",
"parameters": {
"type": "object",
"properties": {
"script": {
"type": "string",
"description": "The Windows PowerShell source to run."
}
},
"required": ["script"]
},
"mutating": true,
"tier": "full_access"
})]
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
Vec::new()
}
}
async fn run_applescript(&self, params: &Value) -> Result<Value, String> {
let script = params
.get("script")
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or("run_applescript requires a non-empty `script`")?;
let lang = match params.get("language").and_then(|v| v.as_str()) {
Some(l)
if l.eq_ignore_ascii_case("javascript")
|| l.eq_ignore_ascii_case("jxa")
|| l.eq_ignore_ascii_case("js") =>
{
car_automation::applescript::Language::JavaScript
}
_ => car_automation::applescript::Language::AppleScript,
};
let out = car_automation::applescript::run(
script,
lang,
Some(std::time::Duration::from_secs(60)),
)
.await
.map_err(|e| format!("automation failed: {e}"))?;
Ok(json!({
"stdout": out.stdout,
"stderr": out.stderr,
"exit_code": out.exit_code,
}))
}
async fn run_powershell(&self, params: &Value) -> Result<Value, String> {
let script = params
.get("script")
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or("run_powershell requires a non-empty `script`")?;
let out = car_automation::powershell::run(script, Some(std::time::Duration::from_secs(60)))
.await
.map_err(|e| format!("automation failed: {e}"))?;
Ok(json!({
"stdout": out.stdout,
"stderr": out.stderr,
"exit_code": out.exit_code,
}))
}
}
#[async_trait]
impl ToolExecutor for AutomationTools {
async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
match tool {
"run_applescript" => self.run_applescript(params).await,
"run_powershell" => self.run_powershell(params).await,
other => Err(format!("unknown tool: '{other}'")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn unknown_tool_falls_through() {
let err = AutomationTools::new()
.execute("nope", &json!({}))
.await
.unwrap_err();
assert!(err.starts_with("unknown tool"), "{err}");
}
#[tokio::test]
async fn rejects_empty_script() {
let err = AutomationTools::new()
.execute("run_applescript", &json!({ "script": " " }))
.await
.unwrap_err();
assert!(err.contains("non-empty"), "{err}");
}
#[test]
fn declares_platform_desktop_tool_at_full_access_tier() {
let defs = AutomationTools::new().tool_defs();
#[cfg(target_os = "macos")]
let expected_name = Some("run_applescript");
#[cfg(target_os = "windows")]
let expected_name = Some("run_powershell");
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let expected_name: Option<&str> = None;
match expected_name {
Some(name) => {
assert_eq!(defs.len(), 1);
assert_eq!(defs[0]["name"], name);
assert_eq!(defs[0]["tier"], "full_access");
assert_eq!(defs[0]["mutating"], true);
}
None => assert!(defs.is_empty()),
}
}
#[cfg(target_os = "windows")]
#[tokio::test]
async fn rejects_empty_powershell_script() {
let err = AutomationTools::new()
.execute("run_powershell", &json!({ "script": " " }))
.await
.unwrap_err();
assert!(err.contains("non-empty"), "{err}");
}
}