security(tool_manager): 禁止自改保留名 + 强制 high risk + 审计日志 + 构建超时清理

P0-2: validate_tool_name 将 tool_manager 等列为保留名,禁止
      create/update/build 作用于自身,杜绝持久化后门。
P0-3: spec_yaml 忽略调用方传入的 risk_level,由 tool_manager
      产出的工具一律 high risk,避免创建低风险后门绕过审批。
审计: 每次调用追加记录到 tools/.tool_audit.log
      (timestamp/user_id/action/tool_name/ok|err/detail)。
构建: 用 timeout 限制 180s,失败时删除残留旧二进制,
      避免 reload 后调用旧版本造成"假成功"。
This commit is contained in:
2026-06-22 10:37:42 +08:00
parent 32c4fcf3a8
commit bf231352fb
2 changed files with 81 additions and 21 deletions
@@ -4,7 +4,7 @@ type: stdio
tool: tool_manager tool: tool_manager
path: /target/release/tool_manager path: /target/release/tool_manager
risk_level: high risk_level: high
timeout_secs: 120 timeout_secs: 300
params: params:
- name: action - name: action
required: true required: true
@@ -23,7 +23,7 @@ params:
desc: 参数列表数组,每项包含 name、required、desc desc: 参数列表数组,每项包含 name、required、desc
- name: risk_level - name: risk_level
required: false required: false
desc: 新工具风险级别,low 或 high,默认 low desc: 已弃用,tool_manager 创建/更新的工具一律强制 high risk,传入值将被忽略
- name: timeout_secs - name: timeout_secs
required: false required: false
desc: 新工具超时时间秒数,默认 30 desc: 新工具超时时间秒数,默认 30
+79 -19
View File
@@ -1,10 +1,22 @@
//! tool_manager — 受控创建、修改、构建本地工具 //! tool_manager — 受控创建、修改、构建本地工具
//!
//! 安全约束:
//! - 不允许操作保留工具名(含 `tool_manager` 自身),防止自改后门
//! - 由本工具创建/更新的工具一律 `risk_level: high`,忽略调用方传入值
//! - 所有调用追加写入 `tools/.tool_audit.log` 审计日志
//! - 构建使用 `timeout` 限制时长,失败时清理残留二进制,避免调用旧版本"假成功"
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; 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() { fn main() {
let input: Value = match serde_json::from_reader(std::io::stdin()) { let input: Value = match serde_json::from_reader(std::io::stdin()) {
Ok(v) => v, Ok(v) => v,
@@ -12,13 +24,20 @@ fn main() {
}; };
let params = &input["params"]; let params = &input["params"];
let user_id = input["user_id"].as_str().unwrap_or("unknown");
let action = params["action"].as_str().unwrap_or(""); let action = params["action"].as_str().unwrap_or("");
let tool_name = params["tool_name"].as_str().unwrap_or(""); let tool_name = params["tool_name"].as_str().unwrap_or("");
let output = match run(action, tool_name, params) { let (output, success) = match run(action, tool_name, params) {
Ok(msg) => msg, Ok(msg) => (msg, true),
Err(e) => format!("工具管理失败: {e}"), 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); result(&output);
} }
@@ -38,7 +57,7 @@ fn run(action: &str, tool_name: &str, params: &Value) -> Result<String, String>
write_tool_project(&tool_dir, tool_name, params)?; write_tool_project(&tool_dir, tool_name, params)?;
build_tool(&tool_dir, tool_name)?; build_tool(&tool_dir, tool_name)?;
Ok(format!( Ok(format!(
"已创建并构建工具 {tool_name}。可通过 query_capabilities 查看并用 call_capability 调用。" "已创建并构建工具 {tool_name}risk_level 强制为 high。可通过 query_capabilities 查看并用 call_capability 调用。"
)) ))
} }
"update_tool" => { "update_tool" => {
@@ -48,7 +67,7 @@ fn run(action: &str, tool_name: &str, params: &Value) -> Result<String, String>
update_tool_project(&tool_dir, tool_name, params)?; update_tool_project(&tool_dir, tool_name, params)?;
build_tool(&tool_dir, tool_name)?; build_tool(&tool_dir, tool_name)?;
Ok(format!( Ok(format!(
"已修改并构建工具 {tool_name}。注册表重载后即可使用最新版本。" "已修改并构建工具 {tool_name}risk_level 强制为 high。注册表重载后即可使用最新版本。"
)) ))
} }
"build_tool" => { "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())?; 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() let should_update_spec = params.get("description").is_some()
|| params.get("params").is_some() || params.get("params").is_some()
|| params.get("risk_level").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> { 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("build")
.arg("--release") .arg("--release")
.current_dir(tool_dir) .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}"))?; .map_err(|e| format!("启动 cargo 失败: {e}"))?;
if !output.status.success() { 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); 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() { if !binary.exists() {
return Err(format!("构建后未找到二进制: {}", binary.display())); 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<PathBuf, String> { fn locate_tools_root() -> Result<PathBuf, String> {
let exe = std::env::current_exe().map_err(|e| e.to_string())?; let exe = std::env::current_exe().map_err(|e| e.to_string())?;
// exe = tools/tool_manager/target/release/tool_manager
let tool_dir = exe let tool_dir = exe
.parent() .parent() // .../target/release
.and_then(Path::parent) .and_then(Path::parent) // .../target
.and_then(Path::parent) .and_then(Path::parent) // .../tool_manager
.ok_or_else(|| "无法定位 tool_manager 目录".to_string())?; .ok_or_else(|| "无法定位 tool_manager 目录".to_string())?;
tool_dir tool_dir
.parent() .parent()
@@ -171,14 +204,13 @@ fn main() {{
} }
fn spec_yaml(tool_name: &str, params: &Value) -> String { 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"] let desc = params["description"]
.as_str() .as_str()
.unwrap_or("由 tool_manager 创建的本地工具"); .unwrap_or("由 tool_manager 创建的本地工具(自动标记 high risk");
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);
let mut out = format!( let mut out = format!(
"name: {tool_name}\n\ "name: {tool_name}\n\
@@ -218,8 +250,10 @@ fn validate_tool_name(name: &str) -> Result<(), String> {
if name.is_empty() { if name.is_empty() {
return Err("缺少 tool_name".to_string()); return Err("缺少 tool_name".to_string());
} }
if matches!(name, "target" | "specs" | "src" | "." | "..") { if RESERVED_NAMES.contains(&name) {
return Err("tool_name 使用了保留名称".to_string()); return Err(format!(
"tool_name '{name}' 是保留名称,禁止 create/update/build"
));
} }
if !name if !name
.chars() .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) { fn result(content: &str) {
println!("{}", json!({"type":"result","content":content})); println!("{}", json!({"type":"result","content":content}));
} }