新增: 无人值守P0任务边界全链

- 任务级授权边界判定与审批旁路单点插桩
- boundary_json 随会话落库(V45 可逆迁移)
- 六路非正常终局统一安全停靠与托盘通知
- 每五轮进度事件与会话头部进度条
- 前端边界配置卡侧栏徽标及 UX 校验补齐
This commit is contained in:
lxy
2026-10-04 16:27:30 +08:00
parent 8396849d04
commit b2d44f85e9
29 changed files with 2773 additions and 142 deletions
+58 -15
View File
@@ -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(行为不变)"),
}