新增: 信任所有目录扩展至敏感目录放行
开关语义升级为完全放行,含 .ssh/.aws 等黑名单目录(产品拍板体验优先,用户知情选择) validate_path 与 is_authorized 双拦截点经进程级标志同源放行,audit 层每轮工具执行前刷新 设置页开启时二次确认,正文明示敏感凭据可进对话上下文风险自担;对话区常驻放行横幅既有 测试以串行闸防并行互踩进程级标志
This commit is contained in:
@@ -11,9 +11,32 @@
|
||||
use super::PathAuthRequest;
|
||||
|
||||
/// 「信任所有目录」总开关 KV key(app_settings,bool,默认关)。
|
||||
/// 开启后白名单外路径不再挂起授权弹窗,黑名单仍硬拒。前端设置页读写同一 key。
|
||||
/// 开启后白名单外路径不再挂起授权弹窗,黑名单拦截也一并放行(产品拍板:体验优先,
|
||||
/// 用户知情选择,后期再收敛)。前端设置页读写同一 key。
|
||||
pub(crate) const PATH_AUTH_BYPASS_KEY: &str = "df-ai-path-auth-bypass";
|
||||
|
||||
/// 黑名单放行的进程级标志。validate_path(run_command 等)所在调用链无 bypass 参数,
|
||||
/// 改签名需穿透全部调用方,故以进程级 AtomicBool 承载;真实来源是 KV PATH_AUTH_BYPASS_KEY,
|
||||
/// 由 audit/mod.rs 每轮工具执行前读 KV 后经 set_blacklist_bypass 刷新,与本文件
|
||||
/// check_file_tool_auth 的入参 bypass 同源同值,两处拦截点语义一致。
|
||||
pub(crate) static PATH_AUTH_BYPASS_ACTIVE: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// 每轮工具执行前由 audit 层把 KV 值同步进进程级标志。
|
||||
pub(crate) fn set_blacklist_bypass(on: bool) {
|
||||
PATH_AUTH_BYPASS_ACTIVE.store(on, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// validate_path 黑名单检查的放行判据(单写多读,Relaxed 序足够)。
|
||||
pub(crate) fn is_blacklist_bypassed() -> bool {
|
||||
PATH_AUTH_BYPASS_ACTIVE.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// 测试串行闸:进程级标志全进程单例,并行测试同时改写标志或依赖其结果会互踩,
|
||||
/// 凡改写标志或依赖标志结果的测试须持锁(仅测试隔离用,非生产路径)。
|
||||
#[cfg(test)]
|
||||
pub(crate) static BLACKLIST_BYPASS_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
/// 提取文件工具的路径参数(用于路径授权预校验)。
|
||||
///
|
||||
/// 仅对走 resolve_workspace_path 校验的文件工具返回路径;非文件工具返回空 Vec(不预校验)。
|
||||
@@ -47,20 +70,21 @@ pub(crate) enum FileToolAuthOutcome {
|
||||
Authorized,
|
||||
/// 未授权 → 附带 PathAuthRequest,供挂起弹窗
|
||||
NeedsAuth(PathAuthRequest),
|
||||
/// 命中黑名单 → 硬拒
|
||||
/// 黑名单命中(bypass 关)或 search_files 盲搜兜底时硬拒
|
||||
Denied(String),
|
||||
}
|
||||
|
||||
/// 对单条文件工具调用做路径授权预校验,返回是否需挂起/拒绝/放行。
|
||||
///
|
||||
/// - 路径任一命中黑名单 → `Denied(reason)`:硬拒(工具返 Err tool_result,不挂起)
|
||||
/// - 路径任一命中黑名单且 bypass 关:`Denied(reason)` 硬拒(工具返 Err tool_result,不挂起)
|
||||
/// - 路径任一未命中白名单(persistent + session_dirs)且非黑名单 → `NeedsAuth`:
|
||||
/// 附带 PathAuthRequest(规范化父目录 + 原始路径列表),供挂起弹窗
|
||||
/// - 全部已授权 → `Authorized`:走原 Low/Med/High 流程
|
||||
/// - `bypass=true`(「信任所有目录」总开关):非黑名单的未授权目录不再产生 NeedsAuth,
|
||||
/// 直接 `Authorized`(search_files 的盲拒同为未授权场景,一并放行);黑名单 Denied 不受影响
|
||||
/// (凭据目录自保护是安全硬边界)。bypass 由调用方 process_tool_calls 读 KV 后传入
|
||||
/// (本函数同步签名,不自取异步 settings)。
|
||||
/// - `bypass=true`(「信任所有目录」总开关):未授权目录不再产生 NeedsAuth,黑名单 Denied
|
||||
/// 也一并放行(产品拍板:开关语义为完全放行,含敏感目录,体验优先,search_files 盲拒
|
||||
/// 随 NeedsAuth 消失一并解除)。bypass 由调用方 process_tool_calls 读 KV 后传入;
|
||||
/// validate_path 无此参数才走全局标志 PATH_AUTH_BYPASS_ACTIVE(与入参同源等价,
|
||||
/// audit 层每轮刷新保证两处拦截点一致)。本函数同步签名,不自取异步 settings。
|
||||
pub(crate) fn check_file_tool_auth(
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
@@ -78,9 +102,16 @@ pub(crate) fn check_file_tool_auth(
|
||||
let mut pending_dirs: Vec<std::path::PathBuf> = Vec::new();
|
||||
for p in &paths {
|
||||
match check_path_authorization(p, allowed) {
|
||||
PathAuthDecision::Denied { reason } => return FileToolAuthOutcome::Denied(reason),
|
||||
PathAuthDecision::Denied { reason } => {
|
||||
// 总开关开启时黑名单一并放行(产品拍板:体验优先,用户知情选择);关闭时硬拒不变。
|
||||
// 此处用入参 bypass;validate_path 无参才走全局标志,两者同源等价(audit 每轮刷同一 KV)。
|
||||
if bypass {
|
||||
continue;
|
||||
}
|
||||
return FileToolAuthOutcome::Denied(reason);
|
||||
}
|
||||
PathAuthDecision::NeedsAuthorization { dir } => {
|
||||
// 总开关开启:非黑名单的未授权目录直接放行(黑名单已在上一分支短路硬拒)
|
||||
// 总开关开启:未授权目录直接放行(黑名单的放行在上一分支处理)
|
||||
if bypass {
|
||||
continue;
|
||||
}
|
||||
@@ -113,6 +144,9 @@ mod tests {
|
||||
/// bypass 开:白名单外路径 NeedsAuth 时直接 Authorized;bypass 关:行为不变(仍 NeedsAuth)。
|
||||
#[test]
|
||||
fn test_bypass_on_needs_auth_becomes_authorized() {
|
||||
// is_authorized 已接入全局 bypass 标志,本例基线断言(入参关)途经该读点,
|
||||
// 须持串行闸,防与并行写标志的测试互踩致基线误得 Authorized。
|
||||
let _guard = BLACKLIST_BYPASS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let allowed = crate::state::AllowedDirs::default();
|
||||
let args = serde_json::json!({ "path": "C:/some/unauthorized/dir" });
|
||||
// 关(基线,行为不变):NeedsAuth
|
||||
@@ -127,16 +161,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// bypass 开:黑名单仍 Denied(凭据目录自保护硬边界不受总开关影响)。
|
||||
/// bypass 开:黑名单也放行(语义反转:总开关=完全放行,含敏感目录)。
|
||||
/// 进程级标志须持串行闸并在断言前复位(先取结果再复位再断言,防 panic 泄漏标志污染并行测试)。
|
||||
#[test]
|
||||
fn test_bypass_on_blacklist_still_denied() {
|
||||
fn test_bypass_on_blacklist_also_allowed() {
|
||||
let _guard = BLACKLIST_BYPASS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
set_blacklist_bypass(true);
|
||||
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"),
|
||||
}
|
||||
let outcome_allowed = matches!(
|
||||
check_file_tool_auth("read_file", &args, &allowed, true),
|
||||
FileToolAuthOutcome::Authorized
|
||||
);
|
||||
set_blacklist_bypass(false);
|
||||
assert!(outcome_allowed, "bypass 开时黑名单路径应放行");
|
||||
}
|
||||
|
||||
/// bypass 关:黑名单行为不变(Denied 基线,与改造前一致)。
|
||||
|
||||
Reference in New Issue
Block a user