优化: AI授权目录改进(误弹窗根因+安全加固)
strip_verbatim比对侧收口/黑名单统一/symlink/bind双路径reload/toast接线;cargo test 18✅
This commit is contained in:
@@ -478,6 +478,12 @@ pub async fn ai_approve(
|
||||
// 前端 store listen 刷新列表(仅命中映射的工具 emit,见 emit_data_changed)。
|
||||
if exec_result.is_ok() {
|
||||
emit_data_changed(&app, &approval.tool_name);
|
||||
// MED-1(CR-260620-01 审查): path 绑定工具(Medium)审批执行后 reload 白名单。
|
||||
// bind_dir_to_project / repo.update_field 直写 DB 绕过 project.rs IPC reload,
|
||||
// 此处单点补全 → AI 绑定目录即时进白名单,防后续文件工具误弹窗。
|
||||
if matches!(approval.tool_name.as_str(), "bind_directory" | "create_project" | "update_project") {
|
||||
state.reload_allowed_dirs().await;
|
||||
}
|
||||
}
|
||||
|
||||
// 重新获取锁,替换占位 tool_result 为真实结果(失败时为错误信息,LLM 据此决定下一步)
|
||||
@@ -549,7 +555,7 @@ pub async fn ai_approve(
|
||||
/// with path_auth=Some + emit AiDirAuthRequired)。前端弹窗三选项,用户选择后调本 IPC。
|
||||
///
|
||||
/// `decision`:
|
||||
/// - `"once"`:写入 `PerConvState.session_allowed_dirs`(会话级,随会话销毁)→ 执行工具 → 恢复 loop
|
||||
/// - `"once"`:写入 `AppState.allowed_dirs.session`(进程级会话临时授权,随 active 会话切换/新建/删除由 clear_session_allowed_dirs 清空)→ 执行工具 → 恢复 loop
|
||||
/// - `"always"`:写入持久化 Settings KV `allowed_dirs`(reload_allowed_dirs 同步内存)→ 执行 → 恢复
|
||||
/// - `"deny"`:工具返 Err "用户拒绝路径授权" → 恢复 loop
|
||||
///
|
||||
@@ -637,6 +643,12 @@ pub async fn ai_authorize_dir(
|
||||
audit_finalize(&state, &tool_call_id, audit_status, Some(result_val.to_string())).await;
|
||||
if exec_result.is_ok() {
|
||||
emit_data_changed(&app, &approval.tool_name);
|
||||
// MED-1(CR-260620-01 审查): path 绑定工具(Medium)审批执行后 reload 白名单。
|
||||
// bind_dir_to_project / repo.update_field 直写 DB 绕过 project.rs IPC reload,
|
||||
// 此处单点补全 → AI 绑定目录即时进白名单,防后续文件工具误弹窗。
|
||||
if matches!(approval.tool_name.as_str(), "bind_directory" | "create_project" | "update_project") {
|
||||
state.reload_allowed_dirs().await;
|
||||
}
|
||||
}
|
||||
|
||||
// 替换占位 tool_result 为真实结果
|
||||
|
||||
@@ -130,14 +130,14 @@ fn validate_path(path: &str) -> anyhow::Result<()> {
|
||||
anyhow::bail!("禁止路径遍历 (..)");
|
||||
}
|
||||
|
||||
if lower.contains("\\.ssh")
|
||||
|| lower.contains("\\.aws")
|
||||
|| lower.contains("\\.gnupg")
|
||||
|| lower.contains("\\appdata\\")
|
||||
|| lower.contains("\\programdata\\")
|
||||
|| lower.contains("\\windows\\")
|
||||
|| lower.contains("\\system32\\")
|
||||
{
|
||||
// 敏感系统目录(.ssh/.aws/.gnupg + Windows/ProgramFiles/ProgramData + Unix 核心):
|
||||
// 统一走 state::is_in_system_blacklist 分段匹配(消除 contains 子串误伤如 my\windows\backup,
|
||||
// 与 is_authorized 运行时黑名单同源,单一来源 — 修 agent1 问题4 两套黑名单不一致)
|
||||
if crate::state::is_in_system_blacklist(&PathBuf::from(&normalized)) {
|
||||
anyhow::bail!("禁止访问敏感系统目录");
|
||||
}
|
||||
// AppData 保留单独 contains(用户级数据,分段匹配会误伤 D:\backup\appdata 这类合法目录名)
|
||||
if lower.contains("\\appdata\\") {
|
||||
anyhow::bail!("禁止访问敏感系统目录");
|
||||
}
|
||||
Ok(())
|
||||
@@ -542,6 +542,10 @@ fn register_project_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
let stack_opt = args.get("stack").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
|
||||
let repo = df_storage::crud::ProjectRepo::new(&db);
|
||||
let stack = bind_dir_to_project(&repo, id, path, stack_opt).await?;
|
||||
// F-260619-03: 项目绑定目录加 AI 白名单由 reload_allowed_dirs 自动读 projects.bind_directory
|
||||
// 统一处理(优雅:绑定 = 自动授权,reload 时合并)。bind handler 不直接写 allowed_dirs
|
||||
// (register_project_tools 无 allowed_dirs 参数,且 reload 集中更易维护)。
|
||||
// 绑定后触发 reload:通过 app_handle emit 事件让 state reload,或下次工具调用前 reload 兜底。
|
||||
Ok(serde_json::json!({ "id": id, "path": path, "stack": stack, "bound": true }))
|
||||
})
|
||||
})},
|
||||
@@ -1790,14 +1794,20 @@ fn search_files_recursive<'a>(
|
||||
.map_err(|e| anyhow::anyhow!("无法读取目录 {}: {}", path, e))?;
|
||||
while let Some(entry) = dir.next_entry().await? {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let metadata = entry.metadata().await?;
|
||||
let is_dir = metadata.is_dir();
|
||||
// file_type 不跟随 symlink(对比 entry.metadata 会跟随);symlink 目录递归会逃逸出白名单
|
||||
// (workspace 内 symlink → 外部目录),故 symlink 一律跳过(不匹配/不递归)。对齐 list_dir_recursive。
|
||||
let file_type = entry.file_type().await?;
|
||||
if file_type.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
let is_dir = file_type.is_dir();
|
||||
if !is_dir {
|
||||
// 字符串包含匹配(大小写不敏感)
|
||||
if name.to_lowercase().contains(pattern) {
|
||||
*total += 1;
|
||||
if results.len() < max_results {
|
||||
let full_path = std::path::Path::new(path).join(&name).to_string_lossy().into_owned();
|
||||
let metadata = entry.metadata().await?;
|
||||
results.push(serde_json::json!({ "path": full_path, "size": metadata.len() }));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user