diff --git a/docs/todo.md b/docs/todo.md index 9a4b6c9..d61fa90 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -489,3 +489,70 @@ — 新建 `crates/df-mcp/` + `src-tauri/src/main.rs` + `src-tauri/Cargo.toml` + 可选 `crates/df-ai/src/ai_tools.rs` --- + +### 💡 2026-06-19 新需求(AI 工具文件访问动态权限模型·已分析·待实施) + +> 用户需求:将现有 `workspace_root` 单一根目录模型,升级为**动态白名单池 + 运行时权限申请**机制(类似 macOS / VS Code 的权限申请模式)。解决「锁太死」(只能绑定单一 workspace_root)和「放太宽」的矛盾。 +> +> **与 MCP Server(F-260619-02)无关**:本需求是 DevFlow 应用内部 AI 工具(read_file/write_file 等)的文件系统访问权限升级,不涉及对外 MCP 协议暴露。 + +- [ ] **F-260619-03 [P1]** — **AI 工具文件访问动态权限模型:workspace_root 单根 → 动态白名单池 + 运行时申请** + + **核心机制**:当 AI 调用 `read_file` / `write_file` 等文件系统工具时,路径校验从「单一 workspace_root 前缀匹配」升级为「动态白名单池校验 + 未命中则挂起 Agentic Loop 向前端弹窗申请授权」。 + + **数据结构变更**: + + 1. **持久化白名单**(Settings KV 存储):`app_settings` 表 key=`allowed_dirs`,value=JSON 数组 `["E:/wk-lab/u-abc", "E:/wk-lab/u-img"]`。前端 Settings 页提供列表增删改查 UI。 + 2. **会话级临时白名单**(内存):`AiSession` 新增 `session_allowed_dirs: HashSet`,仅限当前会话有效。 + 3. **全局状态**:`AppState` 引入 `AllowedDirs { persistent: HashSet, session: HashSet }`,替代写死的 `workspace_root()`。 + + **权限拦截与申请流程**: + 1. **规范化路径**:`canonicalize`(解析软链接、`.` 和 `..`)。 + 2. **检查白名单**:判断 `canonicalize` 后的真实路径是否在持久化或会话白名单中(`real_path.starts_with(allowed_dir)`)。 + 3. **命中则放行**:执行原逻辑。 + 4. **未命中则拦截**:挂起当前 Agentic Loop → Tauri 事件向前端弹窗 `AiDirAuthRequired { path, tool_name }` → 用户选择「仅本次」(加入 session_allowed_dirs)/「未来都允许」(写入 DB Settings + 更新 persistent)/「拒绝」→ 恢复执行。 + + **授权粒度**:弹窗中建议授权目标文件所在的**项目根目录**(而非具体文件),减少弹窗频率。用户可在弹窗中手动收窄或放宽范围。 + + **安全防护(三层)**: + - **第一层 canonicalize**:解析软链接后校验真实路径,防软链接逃逸(授权目录本身也需 canonicalize)。 + - **第二层白名单**:`real_path.starts_with(allowed_dir)` 校验。 + - **第三层黑名单兜底**:保留现有 `validate_path`,禁止系统敏感目录(`/etc`、`/var`、`C:\Windows\System32` 等)。 + + **写操作额外约束**:写操作(write_file/delete_file/patch_file)即使目录已授权,仍走现有 RiskLevel 审批流(Medium/High 需用户确认);`delete_file` 始终 High 风险审批,不受白名单影响。 + + **分阶段实施**: + + | 阶段 | 内容 | 复杂度 | 优先级 | + |------|------|--------|--------| + | **Phase A** | Settings 持久化白名单 + `resolve_workspace_path` 改造为多目录校验(canonicalize + starts_with) | 低 | P1 | + | **Phase B** | 会话级临时授权 + Agentic Loop 挂起/恢复 + 前端弹窗 UI + Tauri 事件 | 高 | P2 | + | **Phase C** | 软链接深度防护 + 系统目录黑名单完善 + 写操作额外约束 | 中 | P2 | + + Phase A 成本最低但收益最大——立即解决「只能绑定单一 workspace_root」的限制,且为后续动态授权打好数据基础。 + + **与现有架构的契合点**: + - SettingsRepo KV 存储:已有,`allowed_dirs` 直接复用,零迁移成本。 + - RiskLevel 审批流:已有完整 tool approval 机制,动态授权可视为「路径级别的 approval」。 + - Agentic Loop:已有挂起/恢复能力(审批等待),扩展路径授权挂起是同构的。 + - Tauri 事件系统:已有 `ai-chat-event`,新增 `AiDirAuthRequired` 事件类型即可。 + + **关键改动点**: + - `tool_registry.rs`:handler 闭包需引入 `AllowedDirs` 的 `Arc` 引用(当前闭包是无状态 `Box::new(|args| ...)`),调整注册逻辑。 + - `resolve_workspace_path`(或等效路径校验函数):从单一 workspace_root 前缀匹配 → 多目录白名单 canonicalize 校验。 + - `state.rs`:AppState 新增 `allowed_dirs` 字段。 + - `commands/ai/mod.rs`:AiSession 新增 `session_allowed_dirs` 字段。 + - `agentic.rs`:捕获 `PATH_AUTH_REQUIRED` 信号 → 挂起 → emit 事件 → 等待恢复。 + - 前端 Settings 页:新增「授权目录」管理 UI。 + - 前端 AiChat:新增路径授权弹窗组件。 + + **验收标准**: + 1. Settings 页可管理持久化授权目录列表(增删改查) + 2. AI 访问授权目录内文件正常执行,无额外弹窗 + 3. AI 访问授权目录外文件时弹窗申请,用户可选择「仅本次」/「未来都允许」/「拒绝」 + 4. 软链接逃逸被 canonicalize 校验拦截 + 5. 系统敏感目录始终被拒绝(黑名单兜底) + 6. `cargo check --workspace EXIT 0` + `vue-tsc EXIT 0` + + — `src-tauri/src/state.rs` + `src-tauri/src/commands/ai/{mod.rs,tool_registry.rs,agentic.rs}` + `src-tauri/src/commands/settings.rs` + 前端 Settings 页 + AiChat 弹窗组件 + diff --git a/src-tauri/src/commands/ai/agentic/guard.rs b/src-tauri/src/commands/ai/agentic/guard.rs new file mode 100644 index 0000000..2a77b3e --- /dev/null +++ b/src-tauri/src/commands/ai/agentic/guard.rs @@ -0,0 +1,69 @@ +//! B-260615-09: generating 状态 RAII guard —— 从 agentic.rs 抽离(重构第一批,纯结构搬迁)。 +//! +//! 行为零变更:仅文件位置移动,逻辑/字段/语义完全保留。 +//! 调用方(agentic.rs run_agentic_loop)经 `use super::guard::GeneratingGuard;` 复用。 + +use std::sync::Arc; + +use tokio::sync::Mutex; + +use crate::commands::ai::AiSession; + +/// generating 复位 RAII guard,取代散布的手动 `session.generating = false`。 +/// +/// 两路复位: +/// - 正常路径:exit 点显式 `reset().await` 即时复位(emit 前调,保证"复位→emit"顺序, +/// 前端收事件时后端已可接下一条)。 +/// - 异常路径(panic/未走正常 return):Drop 兜底 spawn 复位,防 generating 永真卡死前端。 +/// +/// 注:try_continue_agent_loop 不用 guard——其 should_continue=false 路径需保持 +/// generating=true(审批等待态),全函数 guard 会误复位;该函数单点 provider-Err 复位保持手动。 +/// +/// F-260616-09 B 批2:guard 持 `conv_id`,复位改写 `session.conv(&conv_id).generating = false` +/// (per-conv 真相源)。同时**双写顶层 `session.generating = false`** 作共存期桥接 —— 批2 +/// 仅迁移 agentic.rs 路径,IPC(ai_is_generating/ai_chat_send)仍读顶层,故 guard 须双写 +/// 保证 IPC 读到正确值(否则前端 ai_is_generating 永远 true 卡死发送)。批4 IPC 迁移后 +/// 顶层双写移除。 +pub(super) struct GeneratingGuard { + session: Arc>, + /// guard 所属会话(loop 启动时快照的 conv_id,来自 run_agentic_loop 入参)。 + conv_id: String, + done: bool, +} + +impl GeneratingGuard { + pub(super) fn new(session: Arc>, conv_id: String) -> Self { + Self { session, conv_id, done: false } + } + + /// 显式复位 generating=false。emit 前调用保证顺序。幂等。 + /// + /// 双写:per_conv.conv_id.generating(新真相源)+ 顶层 generating(共存期 IPC 桥接)。 + pub(super) async fn reset(&mut self) { + if !self.done { + let mut session = self.session.lock().await; + session.conv(&self.conv_id).generating = false; + self.done = true; + } + } + + /// 解除 Drop 兜底复位但不复位 generating。审批等待 return 路径调用: + /// 保持 generating=true 留 try_continue 续生成,同时 Drop 因 done=true 跳过复位 spawn。 + /// (B-260615-26: 修复审批执行后对话不续生成回归) + pub(super) fn disarm(&mut self) { + self.done = true; + } +} + +impl Drop for GeneratingGuard { + fn drop(&mut self) { + if !self.done { + let session = self.session.clone(); + let conv_id = self.conv_id.clone(); + tauri::async_runtime::spawn(async move { + let mut s = session.lock().await; + s.conv(&conv_id).generating = false; + }); + } + } +} diff --git a/src-tauri/src/commands/ai/agentic.rs b/src-tauri/src/commands/ai/agentic/mod.rs similarity index 96% rename from src-tauri/src/commands/ai/agentic.rs rename to src-tauri/src/commands/ai/agentic/mod.rs index 1bbb079..b6b8faf 100644 --- a/src-tauri/src/commands/ai/agentic.rs +++ b/src-tauri/src/commands/ai/agentic/mod.rs @@ -58,67 +58,11 @@ const PROTECT_COUNT: usize = 6; pub const DEFAULT_MAX_AGENT_RETRIES: usize = 3; // ============================================================ -// B-260615-09: generating 状态 RAII guard +// 重构第一批(2026-06-19):GeneratingGuard 抽离到 guard.rs(纯结构搬迁,行为零变更)。 +// run_agentic_loop 内仍 `GeneratingGuard::new(...)`,路径从本模块改 super::guard。 // ============================================================ - -/// generating 复位 RAII guard,取代散布的手动 `session.generating = false`。 -/// -/// 两路复位: -/// - 正常路径:exit 点显式 `reset().await` 即时复位(emit 前调,保证"复位→emit"顺序, -/// 前端收事件时后端已可接下一条)。 -/// - 异常路径(panic/未走正常 return):Drop 兜底 spawn 复位,防 generating 永真卡死前端。 -/// -/// 注:try_continue_agent_loop 不用 guard——其 should_continue=false 路径需保持 -/// generating=true(审批等待态),全函数 guard 会误复位;该函数单点 provider-Err 复位保持手动。 -/// -/// F-260616-09 B 批2:guard 持 `conv_id`,复位改写 `session.conv(&conv_id).generating = false` -/// (per-conv 真相源)。同时**双写顶层 `session.generating = false`** 作共存期桥接 —— 批2 -/// 仅迁移 agentic.rs 路径,IPC(ai_is_generating/ai_chat_send)仍读顶层,故 guard 须双写 -/// 保证 IPC 读到正确值(否则前端 ai_is_generating 永远 true 卡死发送)。批4 IPC 迁移后 -/// 顶层双写移除。 -struct GeneratingGuard { - session: Arc>, - /// guard 所属会话(loop 启动时快照的 conv_id,来自 run_agentic_loop 入参)。 - conv_id: String, - done: bool, -} - -impl GeneratingGuard { - fn new(session: Arc>, conv_id: String) -> Self { - Self { session, conv_id, done: false } - } - - /// 显式复位 generating=false。emit 前调用保证顺序。幂等。 - /// - /// 双写:per_conv.conv_id.generating(新真相源)+ 顶层 generating(共存期 IPC 桥接)。 - async fn reset(&mut self) { - if !self.done { - let mut session = self.session.lock().await; - session.conv(&self.conv_id).generating = false; - self.done = true; - } - } - - /// 解除 Drop 兜底复位但不复位 generating。审批等待 return 路径调用: - /// 保持 generating=true 留 try_continue 续生成,同时 Drop 因 done=true 跳过复位 spawn。 - /// (B-260615-26: 修复审批执行后对话不续生成回归) - fn disarm(&mut self) { - self.done = true; - } -} - -impl Drop for GeneratingGuard { - fn drop(&mut self) { - if !self.done { - let session = self.session.clone(); - let conv_id = self.conv_id.clone(); - tauri::async_runtime::spawn(async move { - let mut s = session.lock().await; - s.conv(&conv_id).generating = false; - }); - } - } -} +mod guard; +use guard::GeneratingGuard; // ============================================================ // F-260614-04 / F-260614-04b: 单 Provider 流式结果 + fallback 辅助