diff --git a/.blueprint/modules/governance-feedback-loop.md b/.blueprint/modules/governance-feedback-loop.md index e276617..546f946 100644 --- a/.blueprint/modules/governance-feedback-loop.md +++ b/.blueprint/modules/governance-feedback-loop.md @@ -66,7 +66,7 @@ MANAGED 块警示(5b0b068)、Bug Fix/Feature 分类 + S 级轻量卡 + 汇 - **OpenAI SDK tool_calls 联合类型守卫**:可作 AI 代码审查检查项候选(project_type:web) ### 遗留尾巴 -- src-tauri/ 下 6 处空路径污染残留(.blueprint/.claude/docs/AGENTS.md/CLAUDE.md/lefthook.yml,均 untracked,liangjing.json 指向「agent编排界面(opencode)」)——2026-07-18 会话中删除操作被用户拦下,保留待人工决定 +- ~~src-tauri/ 下空路径污染残留~~ → 2026-07-18 辩证收缩后三步收官:①9 个污染文件已移出(暂存 `%TEMP%\liangjing-pollution-20260718`);②两条空路径僵尸登记(agent编排界面(opencode)/银龙集团应用)已 unregister(磁盘无对应目录,实为死登记);③辩证挖出 **inject 链路漏拦同一空路径洞**(`project_root()` 对空串 win_path 穿透写 cwd,正是 settings.json/liangjing.json 污染的来源)——已单点补 `validate_registered_path`(拒空+拒相对路径)+ 2 条回归测试。空路径事故至此两链路(apply/inject)全堵 - envoy.md 硬门槛已进模板,**需重建安装炼境后 apply 才会分发到子项目**(模板打包在二进制资源) ## 任务卡 diff --git a/src-tauri/src/mcp_inject.rs b/src-tauri/src/mcp_inject.rs index d987c65..3c145ea 100644 --- a/src-tauri/src/mcp_inject.rs +++ b/src-tauri/src/mcp_inject.rs @@ -9,6 +9,22 @@ const LEGACY_KEY_PREFIX: &str = "lian-jing:group-"; // ── 路径解析 ────────────────────────────────────────────────────────────────── +/// 校验并解析登记路径:空串/空白/相对路径一律拒绝。 +/// 空路径经 PathBuf::from("") 会解析到炼境进程 cwd——2026-07-15 空路径登记 +/// 曾把 settings.json/liangjing.json 写进 src-tauri/(apply 链路同日已拦, +/// inject 链路本处单点补防)。 +fn validate_registered_path(raw: &str, field: &str) -> Result { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Err(format!("{field} 为空,拒绝解析(会写进炼境进程 cwd)")); + } + let path = PathBuf::from(trimmed); + if !path.is_absolute() { + return Err(format!("{field} 非绝对路径,拒绝解析: {trimmed:?}")); + } + Ok(path) +} + /// 解析项目根目录路径(WSL 项目自动转为 Windows UNC 路径) fn project_root(project_id: &str) -> Result { let conn = db::pool().get().map_err(|e| e.to_string())?; @@ -24,6 +40,9 @@ fn project_root(project_id: &str) -> Result { if platform.as_deref() == Some("wsl") { let wsl = wsl_path.ok_or_else(|| "WSL 项目未配置 wsl_path".to_string())?; + if wsl.trim().is_empty() { + return Err("wsl_path 为空,拒绝解析(会指向 WSL 发行版根目录)".to_string()); + } let distro: String = conn .query_row( "SELECT value FROM settings WHERE key = 'wsl_distro'", @@ -38,7 +57,7 @@ fn project_root(project_id: &str) -> Result { ))) } else { let win = win_path.ok_or_else(|| "Windows 项目未配置 win_path".to_string())?; - Ok(PathBuf::from(win)) + validate_registered_path(&win, "win_path") } } @@ -429,3 +448,30 @@ pub fn reinject_all_mcp() -> Vec { } errors } + +#[cfg(test)] +mod tests { + use super::*; + + /// 回归:空路径登记曾使 inject 链路把 settings.json/liangjing.json 写进 + /// 炼境进程 cwd(src-tauri/,2026-07-15 实锤,apply 链路同日已拦本处漏拦)。 + #[test] + fn registered_path_rejects_empty_and_relative() { + assert!(validate_registered_path("", "win_path").is_err(), "空串应拒绝"); + assert!(validate_registered_path(" ", "win_path").is_err(), "空白应拒绝"); + assert!( + validate_registered_path("src-tauri", "win_path").is_err(), + "相对路径应拒绝(相对炼境 cwd 解析)" + ); + } + + #[test] + fn registered_path_accepts_absolute_and_unc() { + assert!(validate_registered_path("E:\\code-project\\demo", "win_path").is_ok()); + assert!(validate_registered_path("E:/code-project/demo", "win_path").is_ok()); + assert!( + validate_registered_path("\\\\wsl.localhost\\Ubuntu\\home\\u\\p", "win_path").is_ok(), + "WSL UNC 路径应通过" + ); + } +}