Skip to main content

crabtalk_runtime/os/
tool.rs

1//! Tool schemas and input types for OS tools.
2
3use crate::{RuntimeHook, bridge::RuntimeBridge};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use std::collections::BTreeMap;
7use wcore::{
8    agent::{AsTool, ToolDescription},
9    model::Tool,
10};
11
12#[derive(Deserialize, JsonSchema)]
13pub struct Bash {
14    /// Shell command to run (e.g. `"ls -la"`, `"cat foo.txt | grep bar"`).
15    pub command: String,
16    /// Environment variables to set for the process.
17    #[serde(default)]
18    pub env: BTreeMap<String, String>,
19}
20
21impl ToolDescription for Bash {
22    const DESCRIPTION: &'static str = "Run a shell command.";
23}
24
25pub fn tools() -> Vec<Tool> {
26    vec![Bash::as_tool()]
27}
28
29impl<B: RuntimeBridge> RuntimeHook<B> {
30    /// Dispatch a `bash` tool call — run a command directly.
31    pub async fn dispatch_bash(&self, args: &str, session_id: Option<u64>) -> String {
32        let input: Bash = match serde_json::from_str(args) {
33            Ok(v) => v,
34            Err(e) => return format!("invalid arguments: {e}"),
35        };
36        let session_cwd = if let Some(id) = session_id {
37            self.bridge.session_cwd(id)
38        } else {
39            None
40        };
41        let cwd = session_cwd.as_deref().unwrap_or(&self.cwd);
42
43        let mut cmd = tokio::process::Command::new("bash");
44        cmd.args(["-c", &input.command])
45            .envs(&input.env)
46            .current_dir(cwd)
47            .stdout(std::process::Stdio::piped())
48            .stderr(std::process::Stdio::piped());
49
50        let child = match cmd.spawn() {
51            Ok(c) => c,
52            Err(e) => {
53                return serde_json::json!({
54                    "stdout": "",
55                    "stderr": format!("bash failed: {e}"),
56                    "exit_code": -1
57                })
58                .to_string();
59            }
60        };
61
62        match tokio::time::timeout(std::time::Duration::from_secs(30), child.wait_with_output())
63            .await
64        {
65            Ok(Ok(output)) => {
66                let stdout = String::from_utf8_lossy(&output.stdout);
67                let stderr = String::from_utf8_lossy(&output.stderr);
68                let exit_code = output.status.code().unwrap_or(-1);
69                serde_json::json!({
70                    "stdout": stdout.as_ref(),
71                    "stderr": stderr.as_ref(),
72                    "exit_code": exit_code
73                })
74                .to_string()
75            }
76            Ok(Err(e)) => serde_json::json!({
77                "stdout": "",
78                "stderr": format!("bash failed: {e}"),
79                "exit_code": -1
80            })
81            .to_string(),
82            Err(_) => serde_json::json!({
83                "stdout": "",
84                "stderr": "bash timed out after 30 seconds",
85                "exit_code": -1
86            })
87            .to_string(),
88        }
89    }
90}