新增: 无人值守P0任务边界全链
- 任务级授权边界判定与审批旁路单点插桩 - boundary_json 随会话落库(V45 可逆迁移) - 六路非正常终局统一安全停靠与托盘通知 - 每五轮进度事件与会话头部进度条 - 前端边界配置卡侧栏徽标及 UX 校验补齐
This commit is contained in:
@@ -59,22 +59,38 @@ pub(crate) enum FileToolAuthOutcome {
|
||||
Denied(String),
|
||||
}
|
||||
|
||||
/// 「信任所有目录」总开关的放行面(P3-4 按策略分流)。
|
||||
///
|
||||
/// - `Off`:开关关闭(默认),白名单外挂起授权 + 黑名单硬拒,行为与历史版本一致;
|
||||
/// - `All`:主会话全放行(体验优先,黑名单一并放行,产品拍板);
|
||||
/// - `KeepBlacklist`:子代理策略——bypass 只豁免白名单外挂起(含 search_files 盲拒),
|
||||
/// 黑名单保持硬拒。子代理无审批通道也无人监督,黑名单是安全线,不可被体验开关解除。
|
||||
/// Copy:每条工具调用循环内按值传入 check_file_tool_auth,单元枚举按位复制免手动 clone。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum PathBypass {
|
||||
Off,
|
||||
All,
|
||||
KeepBlacklist,
|
||||
}
|
||||
|
||||
/// 对单条文件工具调用做路径授权预校验,返回是否需挂起/拒绝/放行。
|
||||
///
|
||||
/// - 路径任一命中黑名单且 bypass 关:`Denied(reason)` 硬拒(工具返 Err tool_result,不挂起)
|
||||
/// - 路径任一命中黑名单且 bypass 非 `All`:`Denied(reason)` 硬拒(工具返 Err tool_result,不挂起)
|
||||
/// - 路径任一未命中白名单(persistent + session_dirs)且非黑名单 → `NeedsAuth`:
|
||||
/// 附带 PathAuthRequest(规范化父目录 + 原始路径列表),供挂起弹窗
|
||||
/// - 全部已授权 → `Authorized`:走原 Low/Med/High 流程
|
||||
/// - `bypass=true`(「信任所有目录」总开关):未授权目录不再产生 NeedsAuth,黑名单 Denied
|
||||
/// 也一并放行(产品拍板:开关语义为完全放行,含敏感目录,体验优先,search_files 盲拒
|
||||
/// 随 NeedsAuth 消失一并解除)。bypass 由调用方 process_tool_calls 读 KV 后传入;
|
||||
/// validate_path 无此参数才走全局标志 PATH_AUTH_BYPASS_ACTIVE(与入参同源等价,
|
||||
/// audit 层每轮刷新保证两处拦截点一致)。本函数同步签名,不自取异步 settings。
|
||||
/// - `bypass=All`(「信任所有目录」总开关,主会话):未授权目录不再产生 NeedsAuth,黑名单
|
||||
/// Denied 也一并放行(产品拍板:开关语义为完全放行,含敏感目录,体验优先,search_files 盲拒
|
||||
/// 随 NeedsAuth 消失一并解除)。`bypass=KeepBlacklist`(子代理):只豁免 NeedsAuth,
|
||||
/// 黑名单硬拒保持。bypass 由调用方 process_tool_calls 读 KV 后按策略分流传入;
|
||||
/// validate_path 无此参数才走全局标志 PATH_AUTH_BYPASS_ACTIVE(主会话语义,与 All 同源等价,
|
||||
/// audit 层每轮刷新保证两处拦截点一致;子代理文件工具在预校验层已硬拒,handler 级标志不生效)。
|
||||
/// 本函数同步签名,不自取异步 settings。
|
||||
pub(crate) fn check_file_tool_auth(
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
allowed: &crate::state::AllowedDirs,
|
||||
bypass: bool,
|
||||
bypass: PathBypass,
|
||||
) -> FileToolAuthOutcome {
|
||||
use crate::state::{check_path_authorization, PathAuthDecision};
|
||||
let paths = extract_file_tool_paths(tool_name, args);
|
||||
@@ -88,16 +104,17 @@ pub(crate) fn check_file_tool_auth(
|
||||
for p in &paths {
|
||||
match check_path_authorization(p, allowed) {
|
||||
PathAuthDecision::Denied { reason } => {
|
||||
// 总开关开启时黑名单一并放行(产品拍板:体验优先,用户知情选择);关闭时硬拒不变。
|
||||
// 总开关=All 时黑名单一并放行(产品拍板:体验优先,用户知情选择,仅主会话);
|
||||
// 关闭与 KeepBlacklist(子代理)时硬拒不变——黑名单是安全线。
|
||||
// 此处用入参 bypass;validate_path 无参才走全局标志,两者同源等价(audit 每轮刷同一 KV)。
|
||||
if bypass {
|
||||
if matches!(bypass, PathBypass::All) {
|
||||
continue;
|
||||
}
|
||||
return FileToolAuthOutcome::Denied(reason);
|
||||
}
|
||||
PathAuthDecision::NeedsAuthorization { dir } => {
|
||||
// 总开关开启:未授权目录直接放行(黑名单的放行在上一分支处理)
|
||||
if bypass {
|
||||
// 总开关开启(All 或 KeepBlacklist):未授权目录直接放行(黑名单的放行仅在 All 分支)
|
||||
if !matches!(bypass, PathBypass::Off) {
|
||||
continue;
|
||||
}
|
||||
if !pending_dirs.contains(&dir) {
|
||||
@@ -136,17 +153,43 @@ mod tests {
|
||||
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) {
|
||||
match check_file_tool_auth("read_file", &args, &allowed, PathBypass::Off) {
|
||||
FileToolAuthOutcome::NeedsAuth(_) => {}
|
||||
_ => panic!("bypass 关时未授权路径应仍返 NeedsAuth"),
|
||||
}
|
||||
// 开:直接 Authorized,不挂起
|
||||
match check_file_tool_auth("read_file", &args, &allowed, true) {
|
||||
match check_file_tool_auth("read_file", &args, &allowed, PathBypass::All) {
|
||||
FileToolAuthOutcome::Authorized => {}
|
||||
_ => panic!("bypass 开时未授权路径应直接 Authorized"),
|
||||
}
|
||||
}
|
||||
|
||||
/// P3-4 SubAgent 分流:KeepBlacklist 只豁免白名单外挂起(不再弹授权),黑名单保持硬拒
|
||||
/// ——子代理无审批通道也无人监督,黑名单安全线不得被体验开关解除。
|
||||
#[test]
|
||||
fn test_bypass_keep_blacklist_passes_needs_auth_but_denies_blacklist() {
|
||||
let _guard = BLACKLIST_BYPASS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let allowed = crate::state::AllowedDirs::default_with_root();
|
||||
// 白名单外普通路径:KeepBlacklist 直接放行(不挂起授权)
|
||||
let args = serde_json::json!({ "path": "C:/some/unauthorized/dir" });
|
||||
assert!(
|
||||
matches!(
|
||||
check_file_tool_auth("read_file", &args, &allowed, PathBypass::KeepBlacklist),
|
||||
FileToolAuthOutcome::Authorized
|
||||
),
|
||||
"KeepBlacklist 应豁免白名单外挂起"
|
||||
);
|
||||
// 黑名单路径:仍硬拒(与 All 的语义分界)
|
||||
let secret = serde_json::json!({ "path": "/home/user/.ssh/config" });
|
||||
assert!(
|
||||
matches!(
|
||||
check_file_tool_auth("read_file", &secret, &allowed, PathBypass::KeepBlacklist),
|
||||
FileToolAuthOutcome::Denied(_)
|
||||
),
|
||||
"KeepBlacklist 下黑名单应保持硬拒"
|
||||
);
|
||||
}
|
||||
|
||||
/// bypass 开:黑名单也放行(语义反转:总开关=完全放行,含敏感目录)。
|
||||
/// 进程级标志须持串行闸并在断言前复位(先取结果再复位再断言,防 panic 泄漏标志污染并行测试)。
|
||||
#[test]
|
||||
@@ -157,7 +200,7 @@ mod tests {
|
||||
// .ssh 命中系统黑名单(is_in_system_blacklist)
|
||||
let args = serde_json::json!({ "path": "/home/user/.ssh/config" });
|
||||
let outcome_allowed = matches!(
|
||||
check_file_tool_auth("read_file", &args, &allowed, true),
|
||||
check_file_tool_auth("read_file", &args, &allowed, PathBypass::All),
|
||||
FileToolAuthOutcome::Authorized
|
||||
);
|
||||
set_blacklist_bypass(false);
|
||||
@@ -171,7 +214,7 @@ mod tests {
|
||||
let _guard = BLACKLIST_BYPASS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
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) {
|
||||
match check_file_tool_auth("read_file", &args, &allowed, PathBypass::Off) {
|
||||
FileToolAuthOutcome::Denied(_) => {}
|
||||
_ => panic!("bypass 关时黑名单路径应 Denied(行为不变)"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user