diff --git a/src/daemon.rs b/src/daemon.rs index 9cf1382..6ed1aa5 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -49,6 +49,7 @@ use crate::llm::{ use crate::queue::{ConsumerChannels, EnqueueHandle, MessageKind, PipelineMessage, QueueRunner}; use crate::tools::approval::ApprovalManager; +use crate::tools::executor::ToolRunOutcome; use crate::tools::registry::RegistryManager; use crate::wechat::client::WeChatClient; use std::collections::HashMap; @@ -692,7 +693,7 @@ async fn handle_tool_call( eq.enqueue(msg).await; }) }; - let output = ctx + let outcome = ctx .registry .dispatch( target, @@ -704,11 +705,17 @@ async fn handle_tool_call( &wechat_cb, ) .await; - if target == "tool_manager" && !is_tool_error(&output) { + if target == "tool_manager" && !outcome.is_err() { let count = ctx.registry.reload().await; - format!("{output}\n\n工具注册表已重载,当前可用工具数: {count}") + format!( + "{}\n\n工具注册表已重载,当前可用工具数: {count}", + outcome.into_message() + ) } else { - output + if let ToolRunOutcome::Err { ref kind, .. } = outcome { + tracing::warn!(target:"ias::err","[tool] {target} 执行失败: {kind:?}"); + } + outcome.into_message() } } else { let params: serde_json::Value = serde_json::from_str(&arguments).unwrap_or_default(); @@ -734,7 +741,7 @@ async fn handle_tool_call( eq.enqueue(msg).await; }) }; - let output = ctx + let outcome = ctx .registry .dispatch( &tool_name, @@ -746,11 +753,17 @@ async fn handle_tool_call( &wechat_cb, ) .await; - if tool_name == "tool_manager" && !is_tool_error(&output) { + if tool_name == "tool_manager" && !outcome.is_err() { let count = ctx.registry.reload().await; - format!("{output}\n\n工具注册表已重载,当前可用工具数: {count}") + format!( + "{}\n\n工具注册表已重载,当前可用工具数: {count}", + outcome.into_message() + ) } else { - output + if let ToolRunOutcome::Err { ref kind, .. } = outcome { + tracing::warn!(target:"ias::err","[tool] {tool_name} 执行失败: {kind:?}"); + } + outcome.into_message() } }; @@ -767,25 +780,6 @@ async fn handle_tool_call( .await; } -fn is_tool_error(result: &str) -> bool { - result.starts_with("异常退出") - || result.starts_with("启动失败") - || result.starts_with("工具输出非JSON") - || result.starts_with("HTTP失败") - || result.starts_with("读取stdout失败") - || result.starts_with("stdout超时") - || result.starts_with("进程超时") - || result.starts_with("进程异常") - || result.starts_with("未知消息类型") - || result.starts_with("未知工具") - || result.starts_with("不支持的方法") - || result.starts_with("审批失败") - || result.starts_with("审批超时") - || result.starts_with("操作已被用户取消") - || result.starts_with("缺少必填参数") - || result.starts_with("工具 '") -} - fn unpack_call_capability_params(cp: &serde_json::Value) -> (&str, serde_json::Value) { let target = cp["name"].as_str().unwrap_or(""); let mut params = serde_json::Map::new(); diff --git a/src/main.rs b/src/main.rs index bfa97f6..d1bd959 100644 --- a/src/main.rs +++ b/src/main.rs @@ -529,7 +529,7 @@ async fn cmd_tool_dynamic(name: &str, args: &[String]) { + Send + Sync ) = &|_: &str, _: &str| Box::pin(async {}); - let result = registry + let outcome = registry .dispatch( name, &serde_json::Value::Object(params), @@ -541,28 +541,11 @@ async fn cmd_tool_dynamic(name: &str, args: &[String]) { ) .await; - // 检测是否执行出错(匹配 execute_loop 返回的所有错误前缀) - let is_error = result.starts_with("异常退出") - || result.starts_with("启动失败") - || result.starts_with("工具输出非JSON") - || result.starts_with("HTTP失败") - || result.starts_with("读取stdout失败") - || result.starts_with("stdout超时") - || result.starts_with("进程超时") - || result.starts_with("进程异常") - || result.starts_with("未知消息类型") - || result.starts_with("未知工具") - || result.starts_with("不支持的方法") - || result.starts_with("审批失败") - || result.starts_with("审批超时") - || result.starts_with("操作已被用户取消") - || result.starts_with("缺少必填参数") - || result.starts_with("工具 '"); - - if is_error { - eprintln!("❌ {result}"); - std::process::exit(1); - } else { - println!("{result}"); + match outcome { + crate::tools::executor::ToolRunOutcome::Ok(s) => println!("{s}"), + crate::tools::executor::ToolRunOutcome::Err { message, .. } => { + eprintln!("❌ {message}"); + std::process::exit(1); + } } } diff --git a/src/tools/executor.rs b/src/tools/executor.rs index 00e019f..6c0d0df 100644 --- a/src/tools/executor.rs +++ b/src/tools/executor.rs @@ -5,7 +5,11 @@ //! - `{"type":"db",...}` — 请求数据库操作 //! - `{"type":"result","content":"..."}` — 最终结果 //! -//! 所有消息必须是合法 JSON。审批在 http/db 操作前处理。 +//! 所有消息必须是合法 JSON。 +//! +//! ## 安全边界(授权层) +//! **工具级审批在入口统一执行**:高风险工具在 spawn 之前必须获得用户确认, +//! 与工具后续输出的消息类型(result/http/db)无关 —— 杜绝 stdio→result 绕过。 use std::sync::Arc; use std::time::Duration; @@ -24,6 +28,59 @@ struct HttpResponse { body: Value, } +// ─── 结构化结果 ─── + +/// 工具执行错误类别(供调用方区分,替代字符串前缀匹配) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ToolErrorKind { + SpawnFailed, + StdoutReadFailed, + StdoutTimeout, + ProcessTimeout, + ProcessAbnormal, + NonJsonOutput, + UnknownMessageType, + UnsupportedMethod, + HttpFailed, + ApprovalFailed, + ApprovalTimeout, + ApprovalRejected, + MaxRoundsExceeded, + UnknownTool, + MissingParams, +} + +/// 工具执行返回值:成功带内容,失败带类别与消息 +#[derive(Debug, Clone)] +pub enum ToolRunOutcome { + Ok(String), + Err { + kind: ToolErrorKind, + message: String, + }, +} + +impl ToolRunOutcome { + pub fn err(kind: ToolErrorKind, message: impl Into) -> Self { + ToolRunOutcome::Err { + kind, + message: message.into(), + } + } + + pub fn is_err(&self) -> bool { + matches!(self, ToolRunOutcome::Err { .. }) + } + + /// 取出最终展示给 LLM 的消息文本 + pub fn into_message(self) -> String { + match self { + ToolRunOutcome::Ok(s) => s, + ToolRunOutcome::Err { message, .. } => message, + } + } +} + pub async fn execute_loop( spec: &ToolSpec, params: &Value, @@ -36,11 +93,32 @@ pub async fn execute_loop( + Send + Sync ), -) -> String { +) -> ToolRunOutcome { + // ── 统一授权边界:高风险工具在 spawn 前必须通过用户审批 ── + // 不依赖后续消息类型,覆盖 stdio/http/db 所有路径 + if spec.is_high_risk() && !approved { + let (code, rx) = match approval.create(user_id, &spec.name).await { + Ok((c, r)) => (c, r), + Err(e) => return ToolRunOutcome::err(ToolErrorKind::ApprovalFailed, format!("审批失败: {e}")), + }; + wechat_send( + user_id, + &format!("⚠️ {}\n确认码: {}\n回复确认码继续,回复0取消", spec.desc, code), + ) + .await; + match rx.await.unwrap_or(ApprovalDecision::Expired) { + ApprovalDecision::Approved => { /* 授权通过,继续执行 */ } + ApprovalDecision::Rejected => { + return ToolRunOutcome::err(ToolErrorKind::ApprovalRejected, "操作已被用户取消") + } + ApprovalDecision::Expired => { + return ToolRunOutcome::err(ToolErrorKind::ApprovalTimeout, "审批超时,操作已取消") + } + } + } + let start = std::time::Instant::now(); - let binary_path = spec.path.as_deref().unwrap_or(&spec.tool); - const MAX_ROUNDS: u32 = 20; let mut response: Option = None; @@ -51,7 +129,10 @@ pub async fn execute_loop( round += 1; if round > MAX_ROUNDS { error!(target: "ias::err", "[loop] 工具 '{}' 超过最大轮次 {}", spec.name, MAX_ROUNDS); - return format!("工具 '{}' 执行轮次过多(>{})", spec.name, MAX_ROUNDS); + return ToolRunOutcome::err( + ToolErrorKind::MaxRoundsExceeded, + format!("工具 '{}' 执行轮次过多(>{})", spec.name, MAX_ROUNDS), + ); } // ── 信封 ── @@ -69,7 +150,12 @@ pub async fn execute_loop( if let Some(c) = &spec.command { cmd.arg("--command").arg(c); } - // 按工具 spec 定义的 env 列表,从当前进程环境推送对应变量 + // 安全:清空继承环境,仅注入 PATH(子进程定位可执行)+ spec 声明的白名单变量, + // 避免工具读取宿主全部 API key、数据库密码等敏感变量 + cmd.env_clear(); + if let Ok(path) = std::env::var("PATH") { + cmd.env("PATH", path); + } for key in &spec.env { match std::env::var(key) { Ok(v) => { @@ -89,7 +175,7 @@ pub async fn execute_loop( Ok(c) => c, Err(e) => { error!(target: "ias::err","[spawn] {e}"); - return format!("启动失败: {e}"); + return ToolRunOutcome::err(ToolErrorKind::SpawnFailed, format!("启动失败: {e}")); } }; @@ -129,11 +215,14 @@ pub async fn execute_loop( Ok(Ok(l)) => l, Ok(Err(e)) => { let _ = child.start_kill(); - return format!("读取stdout失败: {e}"); + return ToolRunOutcome::err(ToolErrorKind::StdoutReadFailed, format!("读取stdout失败: {e}")); } Err(_) => { let _ = child.start_kill(); - return format!("stdout超时({}s)", spec.timeout_secs); + return ToolRunOutcome::err( + ToolErrorKind::StdoutTimeout, + format!("stdout超时({}s)", spec.timeout_secs), + ); } }; @@ -152,12 +241,15 @@ pub async fn execute_loop( Ok(Ok(s)) => s, Ok(Err(e)) => { error!(target: "ias::err","[process] {e}"); - return format!("进程异常: {e}"); + return ToolRunOutcome::err(ToolErrorKind::ProcessAbnormal, format!("进程异常: {e}")); } Err(_) => { let _ = child.start_kill(); error!(target: "ias::err","[timeout] {}s", spec.timeout_secs); - return format!("进程超时({}s)", spec.timeout_secs); + return ToolRunOutcome::err( + ToolErrorKind::ProcessTimeout, + format!("进程超时({}s)", spec.timeout_secs), + ); } }; @@ -168,7 +260,10 @@ pub async fn execute_loop( } else { stderr_trimmed.to_string() }; - return format!("异常退出({}): {}", status.code().unwrap_or(-1), reason); + return ToolRunOutcome::err( + ToolErrorKind::ProcessAbnormal, + format!("异常退出({}): {}", status.code().unwrap_or(-1), reason), + ); } // ── 解析 ── @@ -177,7 +272,10 @@ pub async fn execute_loop( Ok(v) => v, Err(_) => { warn!(target: "ias::err","[non-json] {:.100}", trimmed); - return format!("工具输出非JSON: {:.100}", trimmed); + return ToolRunOutcome::err( + ToolErrorKind::NonJsonOutput, + format!("工具输出非JSON: {:.100}", trimmed), + ); } }; @@ -186,30 +284,11 @@ pub async fn execute_loop( let content = msg["content"].as_str().unwrap_or(""); let elapsed = start.elapsed(); info!(target: "ias::tool", "[done] {} 完成, 耗时 {:?}, {} 轮", spec.name, elapsed, round); - return content.to_string(); + return ToolRunOutcome::Ok(content.to_string()); } Some("http") => { let method = msg["method"].as_str().unwrap_or("GET"); let url = msg["url"].as_str().unwrap_or(""); - let desc = msg["desc"].as_str().unwrap_or(""); - - if spec.is_high_risk() && !approved { - let (code, rx) = match approval.create(user_id, &spec.name).await { - Ok((c, r)) => (c, r), - Err(e) => return format!("审批失败: {e}"), - }; - wechat_send( - user_id, - &format!("⚠️ {}\n确认码: {}\n回复确认码继续,回复0取消", desc, code), - ) - .await; - let d = rx.await.unwrap_or(ApprovalDecision::Expired); - match d { - ApprovalDecision::Approved => { /* 继续执行 */ } - ApprovalDecision::Rejected => return "操作已被用户取消".to_string(), - ApprovalDecision::Expired => return "审批超时,操作已取消".to_string(), - } - } let client = reqwest::Client::builder() .timeout(Duration::from_secs(25)) @@ -227,7 +306,9 @@ pub async fn execute_loop( .await } ("POST", None) => client.post(url).send().await, - (o, _) => return format!("不支持的方法: {o}"), + (o, _) => { + return ToolRunOutcome::err(ToolErrorKind::UnsupportedMethod, format!("不支持的方法: {o}")) + } }; match resp { Ok(r) => { @@ -237,32 +318,13 @@ pub async fn execute_loop( } Err(e) => { error!(target: "ias::err","[http] {e}"); - return format!("HTTP失败: {e}"); + return ToolRunOutcome::err(ToolErrorKind::HttpFailed, format!("HTTP失败: {e}")); } } } Some("db") => { let op = msg["operation"].as_str().unwrap_or(""); let op_params = msg.get("params").cloned().unwrap_or(Value::Null); - let desc = msg["desc"].as_str().unwrap_or(op); - - if spec.is_high_risk() && !approved { - let (code, rx) = match approval.create(user_id, &spec.name).await { - Ok((c, r)) => (c, r), - Err(e) => return format!("审批失败: {e}"), - }; - wechat_send( - user_id, - &format!("⚠️ {}\n确认码: {}\n回复确认码继续,回复0取消", desc, code), - ) - .await; - let d = rx.await.unwrap_or(ApprovalDecision::Expired); - match d { - ApprovalDecision::Approved => { /* 继续执行 */ } - ApprovalDecision::Rejected => return "操作已被用户取消".to_string(), - ApprovalDecision::Expired => return "审批超时,操作已取消".to_string(), - } - } let result = match op { "read_memories" => memory.read_for(user_id, None).await, @@ -271,8 +333,7 @@ pub async fn execute_loop( if c.is_empty() { "缺少 content".into() } else { - let write_result = memory.write_for(user_id, c).await; - write_result + memory.write_for(user_id, c).await } } "delete_memory" => { @@ -290,7 +351,10 @@ pub async fn execute_loop( } _ => { warn!(target: "ias::err","[unknown-type] {}", msg["type"]); - return format!("未知消息类型: {}", msg["type"]); + return ToolRunOutcome::err( + ToolErrorKind::UnknownMessageType, + format!("未知消息类型: {}", msg["type"]), + ); } } } diff --git a/src/tools/registry.rs b/src/tools/registry.rs index f3d4bf3..9ad7cf4 100644 --- a/src/tools/registry.rs +++ b/src/tools/registry.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use crate::context::MemoryStore; use crate::tools::approval::ApprovalManager; -use crate::tools::executor; +use crate::tools::executor::{self, ToolErrorKind, ToolRunOutcome}; use crate::tools::parser; use crate::tools::spec::ToolSpec; use serde_json::Value; @@ -153,16 +153,16 @@ impl Registry { + Send + Sync ), - ) -> String { + ) -> ToolRunOutcome { let spec = match self.get(name) { Some(s) => s, None => { tracing::warn!(target:"ias::registry","未知: {name}"); - return format!("未知工具: {name}"); + return ToolRunOutcome::err(ToolErrorKind::UnknownTool, format!("未知工具: {name}")); } }; if let Some(err) = validate_required_params(spec, params) { - return err; + return ToolRunOutcome::err(ToolErrorKind::MissingParams, err); } executor::execute_loop( spec, @@ -215,20 +215,20 @@ impl RegistryManager { + Send + Sync ), - ) -> String { + ) -> ToolRunOutcome { let spec = { let registry = self.registry.read().await; match registry.get(name) { Some(spec) => spec.clone(), None => { tracing::warn!(target:"ias::registry","未知: {name}"); - return format!("未知工具: {name}"); + return ToolRunOutcome::err(ToolErrorKind::UnknownTool, format!("未知工具: {name}")); } } }; if let Some(err) = validate_required_params(&spec, params) { - return err; + return ToolRunOutcome::err(ToolErrorKind::MissingParams, err); } executor::execute_loop( diff --git a/tools/tool_manager/specs/tool_manager.tool.yaml b/tools/tool_manager/specs/tool_manager.tool.yaml index ba7429f..104f7ee 100644 --- a/tools/tool_manager/specs/tool_manager.tool.yaml +++ b/tools/tool_manager/specs/tool_manager.tool.yaml @@ -5,6 +5,10 @@ tool: tool_manager path: /target/release/tool_manager risk_level: high timeout_secs: 300 +env: + - HOME + - CARGO_HOME + - RUSTUP_HOME params: - name: action required: true