diff --git a/tools/tool_manager/specs/tool_manager.tool.yaml b/tools/tool_manager/specs/tool_manager.tool.yaml index 00cda49..ba7429f 100644 --- a/tools/tool_manager/specs/tool_manager.tool.yaml +++ b/tools/tool_manager/specs/tool_manager.tool.yaml @@ -4,7 +4,7 @@ type: stdio tool: tool_manager path: /target/release/tool_manager risk_level: high -timeout_secs: 120 +timeout_secs: 300 params: - name: action required: true @@ -23,7 +23,7 @@ params: desc: 参数列表数组,每项包含 name、required、desc - name: risk_level required: false - desc: 新工具风险级别,low 或 high,默认 low + desc: 已弃用,tool_manager 创建/更新的工具一律强制 high risk,传入值将被忽略 - name: timeout_secs required: false desc: 新工具超时时间秒数,默认 30 diff --git a/tools/tool_manager/src/main.rs b/tools/tool_manager/src/main.rs index fbe9ac3..bb69e60 100644 --- a/tools/tool_manager/src/main.rs +++ b/tools/tool_manager/src/main.rs @@ -1,10 +1,22 @@ //! tool_manager — 受控创建、修改、构建本地工具 +//! +//! 安全约束: +//! - 不允许操作保留工具名(含 `tool_manager` 自身),防止自改后门 +//! - 由本工具创建/更新的工具一律 `risk_level: high`,忽略调用方传入值 +//! - 所有调用追加写入 `tools/.tool_audit.log` 审计日志 +//! - 构建使用 `timeout` 限制时长,失败时清理残留二进制,避免调用旧版本"假成功" use serde_json::{Value, json}; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +/// 保留工具名 —— 禁止 create/update/build +const RESERVED_NAMES: &[&str] = &["target", "specs", "src", ".", "..", "tool_manager"]; + +/// 构建超时(秒)。需小于 tool_manager 自身的 timeout_secs。 +const BUILD_TIMEOUT_SECS: u64 = 180; + fn main() { let input: Value = match serde_json::from_reader(std::io::stdin()) { Ok(v) => v, @@ -12,13 +24,20 @@ fn main() { }; let params = &input["params"]; + let user_id = input["user_id"].as_str().unwrap_or("unknown"); let action = params["action"].as_str().unwrap_or(""); let tool_name = params["tool_name"].as_str().unwrap_or(""); - let output = match run(action, tool_name, params) { - Ok(msg) => msg, - Err(e) => format!("工具管理失败: {e}"), + let (output, success) = match run(action, tool_name, params) { + Ok(msg) => (msg, true), + Err(e) => (format!("工具管理失败: {e}"), false), }; + + // 审计日志(best effort,不影响主流程) + if let Ok(tools_root) = locate_tools_root() { + audit(&tools_root, user_id, action, tool_name, success, &output); + } + result(&output); } @@ -38,7 +57,7 @@ fn run(action: &str, tool_name: &str, params: &Value) -> Result write_tool_project(&tool_dir, tool_name, params)?; build_tool(&tool_dir, tool_name)?; Ok(format!( - "已创建并构建工具 {tool_name}。可通过 query_capabilities 查看并用 call_capability 调用。" + "已创建并构建工具 {tool_name}(risk_level 强制为 high)。可通过 query_capabilities 查看并用 call_capability 调用。" )) } "update_tool" => { @@ -48,7 +67,7 @@ fn run(action: &str, tool_name: &str, params: &Value) -> Result update_tool_project(&tool_dir, tool_name, params)?; build_tool(&tool_dir, tool_name)?; Ok(format!( - "已修改并构建工具 {tool_name}。注册表重载后即可使用最新版本。" + "已修改并构建工具 {tool_name}(risk_level 强制为 high)。注册表重载后即可使用最新版本。" )) } "build_tool" => { @@ -83,6 +102,7 @@ fn update_tool_project(tool_dir: &Path, tool_name: &str, params: &Value) -> Resu fs::write(tool_dir.join("src/main.rs"), code).map_err(|e| e.to_string())?; } + // spec 一旦由 tool_manager 管理,risk_level 恒为 high let should_update_spec = params.get("description").is_some() || params.get("params").is_some() || params.get("risk_level").is_some() @@ -101,7 +121,12 @@ fn update_tool_project(tool_dir: &Path, tool_name: &str, params: &Value) -> Resu } fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> { - let output = Command::new("cargo") + let binary = tool_dir.join("target/release").join(tool_name); + + // 用 `timeout` 限制构建时长,超时返回 124;cargo 及其子进程都会被信号终止 + let output = Command::new("timeout") + .arg(BUILD_TIMEOUT_SECS.to_string()) + .arg("cargo") .arg("build") .arg("--release") .current_dir(tool_dir) @@ -109,11 +134,18 @@ fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> { .map_err(|e| format!("启动 cargo 失败: {e}"))?; if !output.status.success() { + // 构建失败:删除可能残留的旧二进制,避免 reload 后仍调用旧版本造成"假成功" + let _ = fs::remove_file(&binary); + let code = output.status.code().unwrap_or(-1); let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!("cargo build 失败: {}", stderr.trim())); + let reason = if code == 124 { + format!("构建超时({BUILD_TIMEOUT_SECS}s)") + } else { + stderr.trim().to_string() + }; + return Err(format!("cargo build 失败(退出码 {code}): {reason}")); } - let binary = tool_dir.join("target/release").join(tool_name); if !binary.exists() { return Err(format!("构建后未找到二进制: {}", binary.display())); } @@ -122,10 +154,11 @@ fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> { fn locate_tools_root() -> Result { let exe = std::env::current_exe().map_err(|e| e.to_string())?; + // exe = tools/tool_manager/target/release/tool_manager let tool_dir = exe - .parent() - .and_then(Path::parent) - .and_then(Path::parent) + .parent() // .../target/release + .and_then(Path::parent) // .../target + .and_then(Path::parent) // .../tool_manager .ok_or_else(|| "无法定位 tool_manager 目录".to_string())?; tool_dir .parent() @@ -171,14 +204,13 @@ fn main() {{ } fn spec_yaml(tool_name: &str, params: &Value) -> String { + // 安全约束:tool_manager 产出的工具一律高风险,忽略调用方传入的 risk_level + let risk_level = "high"; + let timeout_secs = params["timeout_secs"].as_u64().unwrap_or(30).clamp(1, 300); + let desc = params["description"] .as_str() - .unwrap_or("由 tool_manager 创建的本地工具"); - let risk_level = match params["risk_level"].as_str() { - Some("high") => "high", - _ => "low", - }; - let timeout_secs = params["timeout_secs"].as_u64().unwrap_or(30).clamp(1, 300); + .unwrap_or("由 tool_manager 创建的本地工具(自动标记 high risk)"); let mut out = format!( "name: {tool_name}\n\ @@ -218,8 +250,10 @@ fn validate_tool_name(name: &str) -> Result<(), String> { if name.is_empty() { return Err("缺少 tool_name".to_string()); } - if matches!(name, "target" | "specs" | "src" | "." | "..") { - return Err("tool_name 使用了保留名称".to_string()); + if RESERVED_NAMES.contains(&name) { + return Err(format!( + "tool_name '{name}' 是保留名称,禁止 create/update/build" + )); } if !name .chars() @@ -253,6 +287,32 @@ fn truthy(value: Option<&Value>) -> bool { } } +/// 追加审计日志到 tools 根目录下的 .tool_audit.log +fn audit( + tools_root: &Path, + user_id: &str, + action: &str, + tool_name: &str, + success: bool, + detail: &str, +) { + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + // detail 可能很长,截断到 200 字节避免日志膨胀 + let detail: String = detail.chars().take(200).collect(); + let line = format!( + "{ts}\t{user_id}\t{action}\t{tool_name}\t{}\t{detail}\n", + if success { "ok" } else { "err" } + ); + let path = tools_root.join(".tool_audit.log"); + if let Ok(mut f) = fs::OpenOptions::new().create(true).append(true).open(&path) { + use std::io::Write; + let _ = f.write_all(line.as_bytes()); + } +} + fn result(content: &str) { println!("{}", json!({"type":"result","content":content})); }