rucora_tools/system/
cmd_exec.rs1use async_trait::async_trait;
11use rucora_core::{
12 error::ToolError,
13 tool::{Tool, ToolCategory},
14};
15use serde_json::{Value, json};
16
17use super::shell::{SHELL_TIMEOUT_SECS, execute_shell_command};
18
19pub struct CmdExecTool {
23 pub allowed_prefixes: &'static [&'static str],
27}
28
29impl CmdExecTool {
30 pub fn new() -> Self {
34 Self {
35 allowed_prefixes: &["curl", "curl.exe"],
36 }
37 }
38
39 fn validate_command(&self, cmd: &str) -> Result<(), ToolError> {
44 let t = cmd.trim();
45 let prefix_ok = self
46 .allowed_prefixes
47 .iter()
48 .any(|p| t == *p || t.starts_with(&format!("{p} ")));
49
50 if !prefix_ok {
51 return Err(ToolError::Message(
52 "出于安全考虑,cmd_exec 目前仅允许执行 curl 命令".to_string(),
53 ));
54 }
55
56 let forbidden = ["|", "&&", ";", ">", "<", "`", "$ (", "$(", "\n", "\r"];
57 if forbidden.iter().any(|x| t.contains(x)) {
58 return Err(ToolError::Message(
59 "出于安全考虑,cmd_exec 禁止管道/重定向/链式/多行命令".to_string(),
60 ));
61 }
62
63 Ok(())
64 }
65}
66
67impl Default for CmdExecTool {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73#[async_trait]
74impl Tool for CmdExecTool {
75 fn name(&self) -> &str {
77 "cmd_exec"
78 }
79
80 fn description(&self) -> Option<&str> {
82 Some("执行受限的命令行(当前仅允许 curl)")
83 }
84
85 fn categories(&self) -> &'static [ToolCategory] {
87 &[ToolCategory::System]
88 }
89
90 fn input_schema(&self) -> Value {
95 json!({
96 "type": "object",
97 "properties": {
98 "command": {
99 "type": "string",
100 "description": "要执行的命令行(仅允许以 curl 开头)"
101 },
102 "timeout": {
103 "type": "integer",
104 "description": "超时时间(秒),默认 60 秒",
105 "default": 60
106 }
107 },
108 "required": ["command"]
109 })
110 }
111
112 async fn call(&self, input: Value) -> Result<Value, ToolError> {
113 let command = input
114 .get("command")
115 .and_then(|v| v.as_str())
116 .ok_or_else(|| ToolError::Message("缺少必需的 'command' 字段".to_string()))?;
117
118 let timeout_secs = input
119 .get("timeout")
120 .and_then(|v| v.as_u64())
121 .unwrap_or(SHELL_TIMEOUT_SECS);
122
123 self.validate_command(command)?;
125
126 let mut parts = command.split_whitespace();
128 let executable = parts
129 .next()
130 .ok_or_else(|| ToolError::Message("命令不能为空".to_string()))?;
131 let args: Vec<String> = parts.map(String::from).collect();
132 let result = execute_shell_command(executable, &args, timeout_secs, None).await?;
133
134 Ok(json!({
135 "command": command,
136 "stdout": result.stdout,
137 "stderr": result.stderr,
138 "exit_code": result.exit_code,
139 "success": result.exit_code == 0,
140 "truncated": result.truncated
141 }))
142 }
143}