From c409a675cf994782b6d419b4d279704be02a8613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Thu, 25 Jun 2026 01:19:05 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20workspace=5Froot=20?= =?UTF-8?q?=E5=BC=B1=E5=8C=96=E6=96=B9=E6=A1=88=E2=91=A0=E5=8E=BB=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E7=99=BD=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit default_with_root 不再编译期硬塞 CARGO_MANIFEST_DIR(分发后无效路径)+ reload_allowed_dirs KV 空时不兜底塞。用户首次访问走弹窗三档授权,对齐产品定位(不预设源码目录)。3 测试改显式构造。 --- src-tauri/src/state.rs | 44 +++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index e63f94b..95cd61f 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -450,19 +450,19 @@ impl AllowedDirs { /// Settings KV key:F-260619-03 Phase A 持久化白名单(JSON 字符串数组) pub const SETTINGS_KEY: &'static str = "allowed_dirs"; - /// 仅含 workspace_root 的默认白名单(向后兼容:无 allowed_dirs 时行为不变)。 - /// 用于 mod.rs trust_key_for 等无白名单上下文的旧路径(零回归)。 + /// 空白名单(方案①弱化 workspace_root:不再编译期硬塞开发机 CARGO_MANIFEST_DIR)。 + /// 分发后该路径指向编译机不存在的目录,硬塞反成脏白名单;用户首次访问任意目录 + /// 走弹窗三档授权(once/session/always),对齐产品定位(不预设源码目录)。仅作 init + /// 占位(reload_allowed_dirs 从 KV 覆盖)+ 测试构造基线。 pub fn default_with_root() -> Self { - let mut set = HashSet::new(); - set.insert(workspace_root_path()); - Self { persistent: set, session: HashSet::new(), once: HashSet::new() } + Self { persistent: HashSet::new(), session: HashSet::new(), once: HashSet::new() } } /// 路径是否被授权:persistent 或 session 白名单任一 starts_with 命中即放行。 /// - /// **workspace_root 默认在 persistent**(default_with_root 初始插入,reload_allowed_dirs - /// KV 未配时保持),故首次访问工程根免授权(向后兼容)。用户从白名单删除工程根后, - /// 工程根也需授权(动态白名单完整语义,F-260619-03 收尾:去掉代码硬编码固定放行)。 + /// **方案①弱化后 workspace_root 不再默认授权**(default_with_root 空,reload_allowed_dirs + /// KV 未配时也不塞),首次访问工程根走弹窗三档授权。用户授权(always/session/once)后 + /// 落白名单,后续命中放行(动态白名单完整语义,F-260619-03 收尾)。 /// /// **Phase C 黑名单优先**:即使白名单命中,若路径落入系统敏感目录(Windows /// `\Windows\System32` / `\Program Files\`;Unix `/etc /usr /bin /sbin /boot @@ -705,7 +705,7 @@ impl AppState { // 串联,min(3,3)=3,有效上限同未配置 → 行为零变化。 state.reload_provider_caps().await; // F-260619-03 Phase A: 从 Settings KV 加载持久化授权目录白名单覆盖默认值。 - // 失败(读 KV/解析 JSON 出错)不阻断启动,保持 default_with_root(仅 workspace_root)。 + // 失败(读 KV/解析 JSON 出错)不阻断启动,保持 default_with_root(方案①后为空)。 state.reload_allowed_dirs().await; // P0(设置走查):从 Settings KV 恢复持久化知识库配置覆盖 default(防 8 项重启全丢)。 state.reload_knowledge_config().await; @@ -755,7 +755,7 @@ impl AppState { /// F-260619-03 Phase A: 从 Settings KV `allowed_dirs`(JSON 字符串数组)加载持久化白名单。 /// /// 启动 + Settings IPC `ai_set_allowed_dirs` 写入后调用。解析失败/缺失 → 保持 - /// default_with_root(仅 workspace_root,向后兼容:is_authorized 始终放行 workspace_root)。 + /// default_with_root(方案①后为空,首次访问走弹窗授权)。 /// 每条路径尝试 canonicalize 规范化(防大小写/分隔符差异绕过);canonicalize 失败 /// (目录不存在)回退原字面量 trim(写入后再校验场景:先授权目录路径,目录暂不存在)。 pub async fn reload_allowed_dirs(&self) { @@ -789,10 +789,8 @@ impl AppState { all_dirs.sort(); all_dirs.dedup(); let mut set = HashSet::new(); - // KV 未配(空)时保留 workspace_root(向后兼容:默认根免授权) - if all_dirs.is_empty() { - set.insert(workspace_root_path()); - } + // 方案①:KV + 项目绑定均空时不再硬塞 workspace_root(分发后该路径无效)。 + // 用户首次访问任意目录走弹窗三档授权,授权后落 KV 持久。 for d in all_dirs { let d = d.trim(); if d.is_empty() { @@ -978,12 +976,14 @@ mod tests { // 锁定:workspace_root 始终授权 + persistent 命中放行 + 未命中拒绝。 // ============================================================ - /// workspace_root 路径在 default_with_root 白名单内授权通过 + /// 显式授权 workspace_root 后命中(方案①:default_with_root 不再预设,需显式授权) #[test] fn test_allowed_dirs_workspace_root_authorized() { - let allowed = AllowedDirs::default_with_root(); + let mut set = HashSet::new(); + set.insert(workspace_root_path()); + let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() }; let root = workspace_root_path(); - assert!(allowed.is_authorized(&root), "workspace_root 应被授权"); + assert!(allowed.is_authorized(&root), "显式授权的 workspace_root 应命中"); // workspace 内子路径也应授权(starts_with workspace_root) let child = root.join("src").join("main.rs"); assert!(allowed.is_authorized(&child), "workspace_root 子路径应被授权"); @@ -1100,10 +1100,12 @@ mod tests { // F-260619-03 Phase B/C: check_path_authorization 三态决策测试 // ============================================================ - /// Phase B/C: workspace_root 路径 → Authorized + /// Phase B/C: 显式授权 workspace_root 后,内路径 → Authorized(方案①:不再默认) #[test] fn test_check_path_authorized_workspace() { - let allowed = AllowedDirs::default_with_root(); + let mut set = HashSet::new(); + set.insert(workspace_root_path()); + let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() }; let rel = "src/main.rs"; match check_path_authorization(rel, &allowed) { PathAuthDecision::Authorized => {} @@ -1155,7 +1157,9 @@ mod tests { /// persistent 用动态 workspace_root_path(非硬编码开发机路径),candidate 拼 verbatim 前缀。 #[test] fn test_is_authorized_strips_verbatim_prefix() { - let allowed = AllowedDirs::default_with_root(); + let mut set = HashSet::new(); + set.insert(workspace_root_path()); + let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() }; let child = workspace_root_path().join("src").join("main.rs"); // candidate 带 verbatim 前缀(handler canonicalize 后形态)应命中白名单 let verbatim = PathBuf::from(format!("{}{}", r"\\?\", child.to_string_lossy()));