修复: spawn panic 兜底 + namespace 淘汰提示 + run_command 超时与破坏命令信任门控
This commit is contained in:
@@ -19,7 +19,7 @@ use super::cache::{find_cached_high_risk_result, pending_placeholder_for};
|
||||
use super::diff::build_write_file_diff;
|
||||
use super::reason::build_approval_reason;
|
||||
use super::record::audit_tool_call;
|
||||
use super::super::{AiChatEvent, AiSession, ApprovalKind, PendingApproval, ToolCallDraft, trust_key_for, TrustKey};
|
||||
use super::super::{AiChatEvent, AiSession, ApprovalKind, PendingApproval, ToolCallDraft, is_destructive_command, trust_key_for, TrustKey};
|
||||
|
||||
/// 按 risk_level + auto_exec_mode 判定是否自动执行。
|
||||
///
|
||||
@@ -102,6 +102,22 @@ pub(super) async fn check_trust_hits(
|
||||
conv_id: &str,
|
||||
) -> Option<TrustKey> {
|
||||
let key = trust_key_for(&draft.name, args)?;
|
||||
// 安全门控(run_command 会话信任按目录不按命令的补丁):
|
||||
// run_command 的信任 key 只用 working_dir,一次批准 → 同目录任意命令 auto。
|
||||
// 破坏性命令(rm/del/format/... 不可逆)绝不 auto 放行,即使同目录已批准过,
|
||||
// 也必须每轮走正常审批。命中黑名单直接返 None,让流程继续走 pending 审批。
|
||||
if matches!(key, TrustKey::Execute { .. }) {
|
||||
if let Some(cmd) = args.get("command").and_then(|v| v.as_str()) {
|
||||
if is_destructive_command(cmd) {
|
||||
tracing::info!(
|
||||
tool = %draft.name,
|
||||
new_tool_call_id = %draft.id,
|
||||
"[会话信任] 命中破坏性命令黑名单,跳过 auto 放行(走正常审批)"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 短 lock 读 session_trust(仅 contains 判定,无 await,纳秒级),命中即返 key
|
||||
let hit = {
|
||||
let session = session_arc.lock().await;
|
||||
|
||||
@@ -139,17 +139,33 @@ async fn execute_with_heartbeat(
|
||||
let _guard = HeartbeatGuard { stop, handle: heartbeat };
|
||||
// tools.execute 无 timeout 时,卡死工具(run_command 长命令/read_file 大文件/同步
|
||||
// 阻塞工具)永久挂起 → process_tool_calls 持 session lock 永久 → guard.reset 等 lock → AiCompleted
|
||||
// 永不发 → 前端"回答完卡住/超时清空"。60s timeout 兜底:超时返错误 tool_result,锁释放,loop 续跑。
|
||||
// 心跳 30s 续命前端 watchdog,60s timeout 覆盖绝大多数工具(run_command 已自带 10s 子超时)。
|
||||
match tokio::time::timeout(Duration::from_secs(60), tools.execute(name, args)).await {
|
||||
// 永不发 → 前端"回答完卡住/超时清空"。外层 timeout 兜底:超时返错误 tool_result,锁释放,loop 续跑。
|
||||
// 心跳 30s 续命前端 watchdog。
|
||||
//
|
||||
// 外层 timeout 取值(BUF-run-cmd-timeout):原硬编码 60s 会先于 run_command 自带 timeout_secs
|
||||
// (上限 600s)drop,致 run_command 跑长构建(cargo/npm)永远到不了用户配的 timeout_secs。
|
||||
// run_command 分支从 args.timeout_secs 取值(clamp [60,600],默认 60,与 tool_registry.rs:2707
|
||||
// run_command 内部 clamp 同源),其余工具仍 60s。run_command 内部还有自己的子 timeout,
|
||||
// 外层只需 >= 内部 timeout 即不抢断(run_command 内部超时会返带语义的 tool_result,优于外层裸中止)。
|
||||
let outer_secs: u64 = if name == "run_command" {
|
||||
args.get("timeout_secs")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(60)
|
||||
.clamp(60, 600)
|
||||
} else {
|
||||
60
|
||||
};
|
||||
match tokio::time::timeout(Duration::from_secs(outer_secs), tools.execute(name, args)).await {
|
||||
Ok(result) => result,
|
||||
Err(_elapsed) => {
|
||||
tracing::error!(
|
||||
conv_id = %conv_id,
|
||||
tool = %name,
|
||||
"[ai] 工具执行超时(60s),返回错误 tool_result(防 process_tool_calls 持 session lock 永久卡死)"
|
||||
outer_secs,
|
||||
"[ai] 工具执行超时({}s),返回错误 tool_result(防 process_tool_calls 持 session lock 永久卡死)",
|
||||
outer_secs
|
||||
);
|
||||
Err(anyhow::anyhow!("工具执行超时(60s),已中止(防死锁)"))
|
||||
Err(anyhow::anyhow!("工具执行超时({}s),已中止(防死锁)", outer_secs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user