Skip to main content

rucora_tools/system/
git.rs

1//! Git 工具模块。
2//!
3//! 提供 Git 操作功能,支持常见命令和安全检查。
4
5use async_trait::async_trait;
6use rucora_core::{
7    error::ToolError,
8    tool::{Tool, ToolCategory},
9};
10use serde_json::{Value, json};
11use std::path::{Path, PathBuf};
12
13/// 允许的 Git 命令白名单
14const ALLOWED_COMMANDS: &[&str] = &[
15    "status",
16    "log",
17    "diff",
18    "show",
19    "branch",
20    "remote",
21    "config",
22    "rev-parse",
23    "add",
24    "commit",
25    "checkout",
26    "stash",
27    "reset",
28    "revert",
29    "merge",
30    "rebase",
31    "pull",
32    "push",
33    "fetch",
34    "clone",
35    "init",
36    "describe",
37    "tag",
38];
39
40/// 写入命令列表
41const WRITE_COMMANDS: &[&str] = &[
42    "add", "commit", "checkout", "stash", "reset", "revert", "merge", "rebase", "pull", "push",
43    "fetch", "clone", "init",
44];
45
46/// 禁止的危险参数
47const FORBIDDEN_ARGS: &[&str] = &[
48    "--exec",
49    "--upload-pack",
50    "--receive-pack",
51    "--pager",
52    "--editor",
53    "--no-verify",
54    "--no-gpg-sign",
55    "-c",
56];
57
58/// Git 工具:执行 Git 操作。
59///
60/// 安全限制:
61/// - 仅允许白名单中的命令
62/// - 禁止危险参数(防止命令注入)
63/// - 限制工作目录范围
64/// - 自动检测只读/写入操作
65///
66/// 输入格式:
67/// ```json
68/// {
69///   "command": "status",
70///   "args": ["--porcelain"]
71/// }
72/// ```
73///
74/// 支持命令:
75/// - 只读:status, log, diff, show, branch, remote, config, rev-parse
76/// - 写入:add, commit, checkout, stash, reset, revert, merge, rebase, pull, push, fetch, clone, init
77pub struct GitTool {
78    /// 允许的 Git 仓库根目录(可选,限制操作范围)
79    allowed_roots: Option<Vec<PathBuf>>,
80    /// 是否允许写入操作
81    allow_write: bool,
82}
83
84impl GitTool {
85    /// 创建一个新的 GitTool 实例。
86    pub fn new() -> Self {
87        Self {
88            allowed_roots: None,
89            allow_write: true,
90        }
91    }
92
93    /// 设置允许的 Git 仓库根目录
94    pub fn with_allowed_roots(mut self, roots: Vec<PathBuf>) -> Self {
95        self.allowed_roots = Some(roots);
96        self
97    }
98
99    /// 设置是否允许写入操作
100    pub fn with_allow_write(mut self, allow: bool) -> Self {
101        self.allow_write = allow;
102        self
103    }
104
105    /// 检查命令是否是写入操作
106    fn is_write_command(&self, command: &str) -> bool {
107        WRITE_COMMANDS.contains(&command)
108    }
109
110    /// 验证 Git 命令是否允许
111    fn validate_command(&self, command: &str) -> Result<(), ToolError> {
112        let cmd_lower = command.to_lowercase();
113
114        // 检查是否在白名单中
115        if !ALLOWED_COMMANDS.contains(&cmd_lower.as_str()) {
116            return Err(ToolError::Message(format!(
117                "不支持的 Git 命令:{command}(允许的命令:{ALLOWED_COMMANDS:?})"
118            )));
119        }
120
121        // 检查是否允许写入操作
122        if self.is_write_command(&cmd_lower) && !self.allow_write {
123            return Err(ToolError::Message(format!(
124                "Git 写入操作已被禁用:{command}"
125            )));
126        }
127
128        Ok(())
129    }
130
131    /// 验证路径是否安全
132    fn validate_path(&self, path: &str) -> Result<PathBuf, ToolError> {
133        let path = Path::new(path);
134
135        // 解析为绝对路径
136        let canonical_path = if path.is_absolute() {
137            path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
138        } else {
139            std::env::current_dir()
140                .unwrap_or_else(|_| PathBuf::from("."))
141                .join(path)
142                .canonicalize()
143                .unwrap_or_else(|_| path.to_path_buf())
144        };
145
146        // 如果配置了允许的根目录,检查路径是否在其中
147        if let Some(allowed_roots) = &self.allowed_roots {
148            let is_allowed = allowed_roots
149                .iter()
150                .any(|root| canonical_path.starts_with(root));
151            if !is_allowed {
152                return Err(ToolError::Message(format!(
153                    "Git 仓库路径不在允许的范围内(允许的根目录:{allowed_roots:?})"
154                )));
155            }
156        }
157
158        // 禁止访问系统敏感路径
159        let path_str = canonical_path.to_string_lossy().to_lowercase();
160        let forbidden_prefixes = [
161            "/etc/",
162            "/proc/",
163            "/sys/",
164            "/dev/",
165            "/boot/",
166            "/bin/",
167            "/sbin/",
168            "c:\\windows\\",
169            "c:\\program files",
170        ];
171        for prefix in &forbidden_prefixes {
172            if path_str.starts_with(prefix) {
173                return Err(ToolError::Message(format!(
174                    "禁止在系统敏感路径执行 Git 操作:{}",
175                    canonical_path.display()
176                )));
177            }
178        }
179
180        Ok(canonical_path)
181    }
182
183    /// 清理 Git 参数,防止命令注入
184    fn sanitize_args(&self, args: &[String]) -> Result<Vec<String>, ToolError> {
185        let mut result = Vec::with_capacity(args.len());
186
187        for arg in args {
188            let arg_lower = arg.to_lowercase();
189
190            // 检查禁止的参数
191            for forbidden in FORBIDDEN_ARGS {
192                if arg_lower.starts_with(&forbidden.to_lowercase()) {
193                    return Err(ToolError::Message(format!(
194                        "禁止使用 Git 参数:{arg}(存在安全风险)"
195                    )));
196                }
197            }
198
199            // 检查命令注入特征
200            if arg.contains("$(")
201                || arg.contains('`')
202                || arg.contains('|')
203                || arg.contains(';')
204                || arg.contains("&&")
205                || arg.contains("||")
206                || arg.contains('>')
207                || arg.contains('<')
208                || arg.contains('\n')
209                || arg.contains('\r')
210            {
211                return Err(ToolError::Message(format!(
212                    "参数包含危险字符,可能存在注入风险:{arg}"
213                )));
214            }
215
216            // 检查路径遍历
217            if arg.contains("..\\") || arg.contains("../") {
218                // 允许 git diff HEAD~2..HEAD 这样的用法,但不允许路径遍历
219                if arg.starts_with("..")
220                    || arg.ends_with("..")
221                    || arg.contains("/../")
222                    || arg.contains("\\..\\")
223                {
224                    return Err(ToolError::Message(format!("参数包含路径遍历:{arg}")));
225                }
226            }
227
228            result.push(arg.clone());
229        }
230
231        Ok(result)
232    }
233}
234
235impl Default for GitTool {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241#[async_trait]
242impl Tool for GitTool {
243    /// 返回工具名称。
244    fn name(&self) -> &str {
245        "git"
246    }
247
248    /// 返回工具描述。
249    fn description(&self) -> Option<&str> {
250        Some("执行 Git 操作(有安全限制:命令白名单、参数检查、路径限制)")
251    }
252
253    /// 返回工具分类。
254    fn categories(&self) -> &'static [ToolCategory] {
255        &[ToolCategory::System]
256    }
257
258    /// 返回输入参数的 JSON Schema。
259    fn input_schema(&self) -> Value {
260        json!({
261            "type": "object",
262            "properties": {
263                "command": {
264                    "type": "string",
265                    "description": "Git 命令,如 status、log、add、commit"
266                },
267                "args": {
268                    "type": "array",
269                    "items": {
270                        "type": "string"
271                    },
272                    "description": "命令参数列表"
273                },
274                "path": {
275                    "type": "string",
276                    "description": "Git 仓库路径,默认为当前目录",
277                    "default": "."
278                }
279            },
280            "required": ["command"]
281        })
282    }
283
284    /// 执行 Git 命令。
285    async fn call(&self, input: Value) -> Result<Value, ToolError> {
286        let command = input
287            .get("command")
288            .and_then(|v| v.as_str())
289            .ok_or_else(|| ToolError::Message("缺少必需的 'command' 字段".to_string()))?;
290
291        // 验证命令
292        self.validate_command(command)?;
293
294        let path_str = input.get("path").and_then(|v| v.as_str()).unwrap_or(".");
295        let work_dir = self.validate_path(path_str)?;
296
297        // 解析参数
298        let args_vec: Vec<String> = input
299            .get("args")
300            .and_then(|v| v.as_array())
301            .map(|arr| {
302                arr.iter()
303                    .filter_map(|v| v.as_str().map(String::from))
304                    .collect()
305            })
306            .unwrap_or_default();
307
308        // 清理参数
309        let sanitized_args = self.sanitize_args(&args_vec)?;
310
311        // 构建并执行命令
312        let output = tokio::process::Command::new("git")
313            .arg(command)
314            .args(&sanitized_args)
315            .current_dir(&work_dir)
316            .env_clear()
317            .env("PATH", std::env::var("PATH").unwrap_or_default())
318            .env("HOME", std::env::var("HOME").unwrap_or_default())
319            .env(
320                "USERPROFILE",
321                std::env::var("USERPROFILE").unwrap_or_default(),
322            )
323            .output()
324            .await
325            .map_err(|e| ToolError::Message(format!("Git 命令执行失败:{e}")))?;
326
327        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
328        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
329        let is_write = self.is_write_command(&command.to_lowercase());
330
331        Ok(json!({
332            "command": command,
333            "args": sanitized_args,
334            "work_dir": work_dir.display().to_string(),
335            "stdout": stdout,
336            "stderr": stderr,
337            "success": output.status.success(),
338            "is_write_operation": is_write
339        }))
340    }
341}