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:
@@ -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
|
||||||
|
|||||||
@@ -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}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user