matrixcode_core/tools/bash/
mod.rs1mod 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}