重构: workspace_root 弱化方案①去默认白名单

default_with_root 不再编译期硬塞 CARGO_MANIFEST_DIR(分发后无效路径)+ reload_allowed_dirs KV 空时不兜底塞。用户首次访问走弹窗三档授权,对齐产品定位(不预设源码目录)。3 测试改显式构造。
This commit is contained in:
2026-06-25 01:19:05 +08:00
parent 4c54288a8d
commit c409a675cf

View File

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