Skip to main content

matrixcode_core/tools/bash/
mod.rs

1//! Bash command execution tool
2
3mod validator;
4
5use anyhow::Result;
6use async_trait::async_trait;
7use serde_json::{Value, json};
8use std::time::Duration;
9
10use super::{Tool, ToolDefinition};
11use crate::approval::RiskLevel;
12use crate::truncate::truncate_string_in_place;
13use validator::CommandValidator;
14
15pub use validator::ValidationResult;
16
17pub struct BashTool;
18
19const DEFAULT_TIMEOUT_MS: u64 = 120_000;
20const MAX_TIMEOUT_MS: u64 = 600_000;
21const MAX_OUTPUT: usize = 30_000;
22
23#[async_trait]
24impl Tool for BashTool {
25    fn definition(&self) -> ToolDefinition {
26        ToolDefinition {
27            name: "bash".to_string(),
28            description: "在当前工作目录执行 shell 命令,返回合并的 stdout + stderr。
29
30IMPORTANT: 当有相关专用工具时,不要用此工具运行命令。使用专用工具更好:
31
32| 命令 | 替代工具 |
33|-----|---------|
34| cat/head/tail | read |
35| sed/awk | edit |
36| echo > file | write |
37| find/ls | glob |
38| grep/rg | search |
39
40将此工具保留用于:
41- 构建、测试、git、包管理器操作
42- 系统命令和终端操作
43- 需要 shell 执行的命令
44
45工作目录在命令间持久,但 shell 状态不持久。
46命令通过 `sh -c` 执行,有超时限制(默认 120s,最大 600s)。"
47                .to_string(),
48            parameters: json!({
49                "type": "object",
50                "properties": {
51                    "command": {
52                        "type": "string",
53                        "description": "要执行的 shell 命令"
54                    },
55                    "timeout_ms": {
56                        "type": "integer",
57                        "description": "最大运行时间(毫秒,默认 120000,最大 600000)"
58                    }
59                },
60                "required": ["command"]
61            }),
62            ..Default::default()
63        }
64    }
65
66    async fn execute(&self, params: Value) -> Result<String> {
67        let command = params["command"]
68            .as_str()
69            .ok_or_else(|| anyhow::anyhow!("missing 'command'"))?;
70
71        let validator = CommandValidator::new();
72        let result = validator.validate(command);
73        if !result.allowed {
74            anyhow::bail!("refused: {}", result.reason.unwrap_or("unknown"));
75        }
76
77        let timeout_ms = params["timeout_ms"]
78            .as_u64()
79            .unwrap_or(DEFAULT_TIMEOUT_MS)
80            .min(MAX_TIMEOUT_MS);
81
82        let mut cmd = tokio::process::Command::new("sh");
83        cmd.arg("-c").arg(command).kill_on_drop(true);
84
85        let fut = cmd.output();
86        let output = match tokio::time::timeout(Duration::from_millis(timeout_ms), fut).await {
87            Ok(result) => result?,
88            Err(_) => anyhow::bail!("command timed out after {} ms", timeout_ms),
89        };
90
91        let mut stdout = String::from_utf8_lossy(&output.stdout).into_owned();
92        let stderr = String::from_utf8_lossy(&output.stderr);
93        if !stderr.is_empty() {
94            if !stdout.is_empty() {
95                stdout.push('\n');
96            }
97            stdout.push_str(&stderr);
98        }
99
100        // Filter out ANSI escape sequences that may have leaked from TUI
101        let stdout = filter_ansi_sequences(&truncate_output(stdout));
102
103        let code = output.status.code().unwrap_or(-1);
104        if !output.status.success() {
105            return Ok(format!("[exit {}]\n{}", code, stdout));
106        }
107
108        Ok(stdout)
109    }
110
111    fn risk_level(&self) -> RiskLevel {
112        RiskLevel::Dangerous
113    }
114}
115
116fn truncate_output(mut s: String) -> String {
117    if s.len() <= MAX_OUTPUT {
118        return s;
119    }
120    truncate_string_in_place(&mut s, MAX_OUTPUT);
121    s.push_str(&format!(
122        "\n... (truncated, output exceeded {} bytes)",
123        MAX_OUTPUT
124    ));
125    s
126}
127
128/// Filter ANSI escape sequences from output
129/// These can leak from TUI when bash captures output during rendering
130fn filter_ansi_sequences(s: &str) -> String {
131    // Regex to match ANSI escape sequences: ESC [ ... letter
132    // Also matches cursor movement, color codes, clearing, etc.
133    static ANSI_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
134    let re = ANSI_RE.get_or_init(|| {
135        regex::Regex::new(r"\x1B\[[0-9;]*[A-Za-z]|\x1B\][^\x07]*\x07|\x1B[()][AB012]").unwrap()
136    });
137    
138    // Also filter common control characters that may appear
139    let mut result = String::with_capacity(s.len());
140    for line in s.lines() {
141        let cleaned = re.replace_all(line, "");
142        let cleaned = cleaned.trim();
143        if !cleaned.is_empty() {
144            if !result.is_empty() {
145                result.push('\n');
146            }
147            result.push_str(cleaned);
148        }
149    }
150    result
151}