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        let stdout = truncate_output(stdout);
101
102        let code = output.status.code().unwrap_or(-1);
103        if !output.status.success() {
104            return Ok(format!("[exit {}]\n{}", code, stdout));
105        }
106
107        Ok(stdout)
108    }
109
110    fn risk_level(&self) -> RiskLevel {
111        RiskLevel::Dangerous
112    }
113}
114
115fn truncate_output(mut s: String) -> String {
116    if s.len() <= MAX_OUTPUT {
117        return s;
118    }
119    truncate_string_in_place(&mut s, MAX_OUTPUT);
120    s.push_str(&format!(
121        "\n... (truncated, output exceeded {} bytes)",
122        MAX_OUTPUT
123    ));
124    s
125}