use crate::{Env, host::Host};
use schemars::JsonSchema;
use serde::Deserialize;
use std::collections::BTreeMap;
use wcore::{
agent::{AsTool, ToolDescription},
model::Tool,
};
#[derive(Deserialize, JsonSchema)]
pub struct Bash {
pub command: String,
#[serde(default)]
pub env: BTreeMap<String, String>,
}
impl ToolDescription for Bash {
const DESCRIPTION: &'static str = "Run a shell command.";
}
pub fn tools() -> Vec<Tool> {
vec![Bash::as_tool()]
}
impl<H: Host> Env<H> {
pub async fn dispatch_bash(&self, args: &str, session_id: Option<u64>) -> String {
let input: Bash = match serde_json::from_str(args) {
Ok(v) => v,
Err(e) => return format!("invalid arguments: {e}"),
};
let session_cwd = if let Some(id) = session_id {
self.host.session_cwd(id)
} else {
None
};
let cwd = session_cwd.as_deref().unwrap_or(&self.cwd);
let mut cmd = tokio::process::Command::new("bash");
cmd.args(["-c", &input.command])
.envs(&input.env)
.current_dir(cwd)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
return serde_json::json!({
"stdout": "",
"stderr": format!("bash failed: {e}"),
"exit_code": -1
})
.to_string();
}
};
match tokio::time::timeout(std::time::Duration::from_secs(30), child.wait_with_output())
.await
{
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let exit_code = output.status.code().unwrap_or(-1);
serde_json::json!({
"stdout": stdout.as_ref(),
"stderr": stderr.as_ref(),
"exit_code": exit_code
})
.to_string()
}
Ok(Err(e)) => serde_json::json!({
"stdout": "",
"stderr": format!("bash failed: {e}"),
"exit_code": -1
})
.to_string(),
Err(_) => serde_json::json!({
"stdout": "",
"stderr": "bash timed out after 30 seconds",
"exit_code": -1
})
.to_string(),
}
}
}