新增: AI工具授权目录总开关(信任所有目录)

- path_auth判定加bypass旁路:开启时白名单外目录免逐次询问直接放行
- 黑名单凭据目录保护不随开关放行(安全硬边界保留)
- 设置页授权目录面板加开关(默认关,读取失败按关处理)
- 对话区开启时常驻放行提示横幅防静默裸奔,中英i18n齐
This commit is contained in:
lxy
2026-09-29 14:59:02 +08:00
parent 0168617503
commit 8a1675b58c
8 changed files with 114 additions and 5 deletions
@@ -10,6 +10,10 @@
use super::PathAuthRequest;
/// 「信任所有目录」总开关 KV key(app_settings,bool,默认关)。
/// 开启后白名单外路径不再挂起授权弹窗,黑名单仍硬拒。前端设置页读写同一 key。
pub(crate) const PATH_AUTH_BYPASS_KEY: &str = "df-ai-path-auth-bypass";
/// 提取文件工具的路径参数(用于路径授权预校验)。
///
/// 仅对走 resolve_workspace_path 校验的文件工具返回路径;非文件工具返回空 Vec(不预校验)。
@@ -53,10 +57,15 @@ pub(crate) enum FileToolAuthOutcome {
/// - 路径任一未命中白名单(persistent + session_dirs)且非黑名单 → `NeedsAuth`:
/// 附带 PathAuthRequest(规范化父目录 + 原始路径列表),供挂起弹窗
/// - 全部已授权 → `Authorized`:走原 Low/Med/High 流程
/// - `bypass=true`(「信任所有目录」总开关):非黑名单的未授权目录不再产生 NeedsAuth,
/// 直接 `Authorized`(search_files 的盲拒同为未授权场景,一并放行);黑名单 Denied 不受影响
/// (凭据目录自保护是安全硬边界)。bypass 由调用方 process_tool_calls 读 KV 后传入
/// (本函数同步签名,不自取异步 settings)。
pub(crate) fn check_file_tool_auth(
tool_name: &str,
args: &serde_json::Value,
allowed: &crate::state::AllowedDirs,
bypass: bool,
) -> FileToolAuthOutcome {
use crate::state::{check_path_authorization, PathAuthDecision};
let paths = extract_file_tool_paths(tool_name, args);
@@ -71,6 +80,10 @@ pub(crate) fn check_file_tool_auth(
match check_path_authorization(p, allowed) {
PathAuthDecision::Denied { reason } => return FileToolAuthOutcome::Denied(reason),
PathAuthDecision::NeedsAuthorization { dir } => {
// 总开关开启:非黑名单的未授权目录直接放行(黑名单已在上一分支短路硬拒)
if bypass {
continue;
}
if !pending_dirs.contains(&dir) {
pending_dirs.push(dir);
}
@@ -92,3 +105,48 @@ pub(crate) fn check_file_tool_auth(
FileToolAuthOutcome::NeedsAuth(PathAuthRequest { dirs: pending_dirs, raw_paths: paths })
}
}
#[cfg(test)]
mod tests {
use super::*;
/// bypass 开:白名单外路径 NeedsAuth → 直接 Authorized;bypass 关:行为不变(仍 NeedsAuth)。
#[test]
fn test_bypass_on_needs_auth_becomes_authorized() {
let allowed = crate::state::AllowedDirs::default();
let args = serde_json::json!({ "path": "C:/some/unauthorized/dir" });
// 关(基线,行为不变):NeedsAuth
match check_file_tool_auth("read_file", &args, &allowed, false) {
FileToolAuthOutcome::NeedsAuth(_) => {}
_ => panic!("bypass 关时未授权路径应仍返 NeedsAuth"),
}
// 开:直接 Authorized,不挂起
match check_file_tool_auth("read_file", &args, &allowed, true) {
FileToolAuthOutcome::Authorized => {}
_ => panic!("bypass 开时未授权路径应直接 Authorized"),
}
}
/// bypass 开:黑名单仍 Denied(凭据目录自保护硬边界不受总开关影响)。
#[test]
fn test_bypass_on_blacklist_still_denied() {
let allowed = crate::state::AllowedDirs::default_with_root();
// .ssh 命中系统黑名单(is_in_system_blacklist)
let args = serde_json::json!({ "path": "/home/user/.ssh/config" });
match check_file_tool_auth("read_file", &args, &allowed, true) {
FileToolAuthOutcome::Denied(_) => {}
_ => panic!("bypass 开时黑名单路径应仍 Denied"),
}
}
/// bypass 关:黑名单行为不变(Denied 基线,与改造前一致)。
#[test]
fn test_bypass_off_blacklist_still_denied() {
let allowed = crate::state::AllowedDirs::default_with_root();
let args = serde_json::json!({ "path": "/home/user/.ssh/config" });
match check_file_tool_auth("read_file", &args, &allowed, false) {
FileToolAuthOutcome::Denied(_) => {}
_ => panic!("bypass 关时黑名单路径应 Denied(行为不变)"),
}
}
}