新增: AE-2025-04会话级授权(TrustKey目录级+同会话同类自动放行)

This commit is contained in:
2026-06-17 01:29:30 +08:00
parent 0202b5195e
commit 6ec191e750
9 changed files with 218 additions and 1 deletions

View File

@@ -34,7 +34,7 @@ pub mod stream_recv;
pub mod title;
pub mod tool_registry;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -97,6 +97,11 @@ pub enum AiChatEvent {
AiToolCallStarted { id: String, name: String, args: serde_json::Value, conversation_id: Option<String> },
/// 工具调用完成
AiToolCallCompleted { id: String, result: serde_json::Value, conversation_id: Option<String> },
/// 会话级信任自动放行AE-2025-04 Session Trust
/// 同会话已批准过同类操作(同工具+同目录),下次命中自动放行,前端显示轻量 toast。
/// 与 AiToolCallStarted/Completed 正交——这三个事件描述一次信任放行调用生命周期:
/// Started → AutoApprovedtoast 提示用户已自动放行)→ Completed。
AiToolAutoApproved { id: String, tool: String, dir: String, conversation_id: Option<String> },
/// 需要人工审批
/// diffAE-2025-03路径 B仅 write_file 注入(旧文件 vs 新内容行级 unified diff
/// 供审批卡即时预览。旧文件不存在(新建)或非 write_file 工具为 None。
@@ -171,6 +176,105 @@ pub enum SessionState {
AwaitingApproval,
}
// ============================================================
// AE-2025-04 会话级信任Session Trust
//
// 核心:信任上下文相关。同会话批准过同类操作(同工具+同目录)后自动放行;
// 换会话new/switch清空重审session_trust 随会话销毁,不落库)。
//
// TrustKey 按 操作 + 目录 粒度非文件级——文件级粒度过细LLM 每写一个文件都要重审,
// 信任面太窄失去意义;目录级让"批准过同目录写入"覆盖该目录后续写入,对齐"写→跑→看→改"
// 闭环最高频场景。首批write_file + run_command。
//
// FR-S1 安全边界TrustKey 只含 tool + dir**绝不**含 api_key / 文件内容 / 命令串等敏感
// 参数。dir 经 resolve_workspace_path/canonicalize 规范化(去 .. / symlink 逃逸)。
// ============================================================
/// 会话级信任键:按「操作类型 + 目录」粒度唯一标识一类已批准操作。
///
/// - `Write { dir }`write_file 在 `dir` 目录下(路径父目录归一化)
/// - `Execute { dir }`run_command 在 `dir` 工作目录下
///
/// 命中即自动放行,跳过 pending 审批 + 二次确认AE-05不写 audit 仍记一条
/// `decided_by=auto_trust` 的审计记录留痕。
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum TrustKey {
Write { dir: String },
Execute { dir: String },
}
/// 由「工具名 + args」推算 TrustKey若该工具不在首批信任名单内返 None走原审批流程
///
/// 目录归一化策略(与 tool_registry.rs 各 handler 的路径锚定一致):
/// - write_file取 args.pathresolve_workspace_path → 父目录;解析失败回退 workspace_root
/// - run_command取 args.working_dir缺省回退 workspace_root不做越界校验
/// run_command handler 自身仅 validate_path 不限定 workspace信任目录用 LLM 传入或
/// workspace_root 默认值作为 key 字面量,与 handler 实际 working_dir 对齐)
///
/// FR-S1不读 api_key / content / command 等敏感字段,只取 path/working_dir 归一化为 dir。
pub fn trust_key_for(tool: &str, args: &serde_json::Value) -> Option<TrustKey> {
match tool {
"write_file" => {
let path = args.get("path").and_then(|v| v.as_str())?;
let dir = dir_of_path_normalized(path);
Some(TrustKey::Write { dir })
}
"run_command" => {
let dir = match args.get("working_dir").and_then(|v| v.as_str()) {
Some(d) => normalize_dir_key(d),
None => workspace_root_str(),
};
Some(TrustKey::Execute { dir })
}
_ => None,
}
}
/// write_file 路径 → 父目录规范化字符串(作 HashSet key 用)。
///
/// resolve_workspace_path 失败(路径越界/含 .. )回退 workspace_root —— 失败的工具调用本身
/// 不会被执行handler 内校验拦截),但 trust_key 计算不应 panic失败回退保守默认。
fn dir_of_path_normalized(path: &str) -> String {
match self::tool_registry::resolve_workspace_path_pub(path) {
Ok(resolved) => {
let parent = resolved
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| std::path::PathBuf::from("."));
normalize_dir_key(&parent.to_string_lossy())
}
Err(_) => workspace_root_str(),
}
}
/// run_command / write_file 共用的目录 key 规范化:
/// 尝试 canonicalize去 symlink / .. / 大小写归一),失败回退原字面量 trim。
fn normalize_dir_key(dir: &str) -> String {
let p = std::path::Path::new(dir);
match p.canonicalize() {
Ok(canon) => canon.to_string_lossy().to_string(),
Err(_) => dir.trim_end_matches(['/', '\\']).to_string(),
}
}
/// workspace_root 字符串(作 run_command working_dir 缺省时的 trust key 回退)。
///
/// 与 tool_registry.rs workspace_root() 同源CARGO_MANIFEST_DIR 上两级canonicalize 后
/// 保证与 run_command handler 默认 working_dir = workspace_root().to_string() 生成的 key 一致。
fn workspace_root_str() -> String {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
root.canonicalize()
.map(|c| c.to_string_lossy().to_string())
.unwrap_or_else(|_| root.to_string_lossy().to_string())
}
// 引入 PathBuf 供 workspace_root_str / dir_of_path_normalized 使用
use std::path::PathBuf;
/// AI 会话内状态Mutex 保护)
pub struct AiSession {
/// 对话历史ContextManager唯一消息真相源裁剪仅影响发送视图不影响持久化
@@ -235,6 +339,11 @@ pub struct AiSession {
/// (防上一对话 override 残留污染新对话)。try_continue_agent_loop 读此字段透传给续跑
/// loop(审批续跑/达 max 续跑保持 override 一致,语义=同一主对话)。
pub model_override: Option<String>,
/// AE-2025-04 会话级信任Session Trust内存态随会话销毁不落库。
/// 同会话已批准过同类操作(同工具+同目录 TrustKey 命中)后自动放行,跳过 pending 审批 +
/// 二次确认AE-05。换会话new/switch必须清空重审见 ai_conversation_create /
/// ai_conversation_switch 清字段)。
pub session_trust: HashSet<TrustKey>,
}
impl AiSession {
@@ -251,6 +360,7 @@ impl AiSession {
notify: Arc::new(tokio::sync::Notify::new()),
iteration_used: 0,
model_override: None,
session_trust: HashSet::new(),
}
}