新增: AE-2025-04会话级授权(TrustKey目录级+同会话同类自动放行)
This commit is contained in:
@@ -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 → AutoApproved(toast 提示用户已自动放行)→ Completed。
|
||||
AiToolAutoApproved { id: String, tool: String, dir: String, conversation_id: Option<String> },
|
||||
/// 需要人工审批
|
||||
/// diff(AE-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.path,resolve_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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user