新增: 路径授权三档化 once/session/always
Phase B 弹窗 once/always → once/session/always 三档(本次单次执行后清/当前会话切会话清/始终落KV)。修 once 原写 session 致本次实为本会话名实不符。AllowedDirs 加 once 层 + ai_authorize_dir decision 改 match 四分支 + 执行后 clear_once。
This commit is contained in:
@@ -436,10 +436,14 @@ pub struct AllowedDirs {
|
||||
/// 持久化授权目录(Settings KV `allowed_dirs`,JSON 字符串数组)。
|
||||
/// 已规范化(canonicalize 失败回退原字面量),便于 starts_with 精确比对。
|
||||
pub persistent: HashSet<PathBuf>,
|
||||
/// F-260619-03 Phase B: 会话级临时授权目录(弹窗"仅本次"写入,进程级内存)。
|
||||
/// F-260619-03 Phase B: 会话级临时授权目录(弹窗"当前会话"选项写入,进程级内存)。
|
||||
/// 仅当前活跃会话生效,切换/新建/删除会话由 clear_session_allowed_dirs 清空(不落库)。
|
||||
/// handler 闭包与 process_tool_calls 预校验均读此字段,确保两端授权判定一致。
|
||||
pub session: HashSet<PathBuf>,
|
||||
/// 本次单次授权目录(弹窗"本次"选项写入)。单次工具执行放行后由 clear_once_allowed_dirs
|
||||
/// 清空(用完即弃,下次同路径再访问仍弹窗)。区别于 session(整会话有效)。
|
||||
/// 三档授权语义:once=本次单次 / session=当前会话 / persistent=始终落 KV。
|
||||
pub once: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl AllowedDirs {
|
||||
@@ -451,7 +455,7 @@ impl AllowedDirs {
|
||||
pub fn default_with_root() -> Self {
|
||||
let mut set = HashSet::new();
|
||||
set.insert(workspace_root_path());
|
||||
Self { persistent: set, session: HashSet::new() }
|
||||
Self { persistent: set, session: HashSet::new(), once: HashSet::new() }
|
||||
}
|
||||
|
||||
/// 路径是否被授权:persistent 或 session 白名单任一 starts_with 命中即放行。
|
||||
@@ -484,6 +488,7 @@ impl AllowedDirs {
|
||||
let cand = strip_verbatim(best_effort_canonicalize(candidate));
|
||||
self.persistent.iter().any(|d| cand.starts_with(d))
|
||||
|| self.session.iter().any(|d| cand.starts_with(d))
|
||||
|| self.once.iter().any(|d| cand.starts_with(d))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,7 +811,8 @@ impl AppState {
|
||||
// 仅覆盖 persistent。用户改 Settings 不影响当前会话已临时授权的目录。
|
||||
let mut guard = self.allowed_dirs.write().await;
|
||||
let preserved_session = std::mem::take(&mut guard.session);
|
||||
*guard = AllowedDirs { persistent: set, session: preserved_session };
|
||||
let preserved_once = std::mem::take(&mut guard.once);
|
||||
*guard = AllowedDirs { persistent: set, session: preserved_session, once: preserved_once };
|
||||
}
|
||||
|
||||
/// F-260619-03 Phase A: 写 Settings KV + 同步内存白名单(供 Settings IPC 调用)。
|
||||
@@ -890,6 +896,28 @@ impl AppState {
|
||||
pub async fn clear_session_allowed_dirs(&self) {
|
||||
self.allowed_dirs.write().await.session.clear();
|
||||
}
|
||||
|
||||
/// F-260619-03 Phase B: 追加本次单次授权目录(弹窗"本次"选项)。
|
||||
///
|
||||
/// 写入 `allowed_dirs.once`(进程级内存,不落库)。handler 执行工具时 read lock
|
||||
/// 取快照读 once 放行,执行后由 clear_once_allowed_dirs 清空(本次放行即失效,
|
||||
/// 下次同路径仍弹窗)。规范化与 add_session_allowed_dir 一致(canonicalize 失败回退 trim)。
|
||||
pub async fn add_once_allowed_dir(&self, dir: String) {
|
||||
let d = dir.trim();
|
||||
if d.is_empty() {
|
||||
return;
|
||||
}
|
||||
let p = PathBuf::from(d);
|
||||
let normalized = strip_verbatim(std::fs::canonicalize(&p).unwrap_or_else(|_| {
|
||||
PathBuf::from(d.trim_end_matches(['/', '\\']))
|
||||
}));
|
||||
self.allowed_dirs.write().await.once.insert(normalized);
|
||||
}
|
||||
|
||||
/// F-260619-03 Phase B: 清空本次单次授权(工具执行后调,本次放行即失效)。
|
||||
pub async fn clear_once_allowed_dirs(&self) {
|
||||
self.allowed_dirs.write().await.once.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// F-260619-03 Phase B: 路径字符串等价比较(canonicalize 后比对,失败回退小写比对)。
|
||||
@@ -967,7 +995,7 @@ mod tests {
|
||||
let mut set = HashSet::new();
|
||||
let custom = PathBuf::from("E:/wk-test-external-dir");
|
||||
set.insert(custom.clone());
|
||||
let allowed = AllowedDirs { persistent: set, session: HashSet::new() };
|
||||
let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() };
|
||||
// 精确命中 + 子路径 starts_with 命中
|
||||
assert!(allowed.is_authorized(&custom));
|
||||
assert!(allowed.is_authorized(&custom.join("sub").join("file.txt")));
|
||||
@@ -978,7 +1006,7 @@ mod tests {
|
||||
fn test_allowed_dirs_unauthorized_rejected() {
|
||||
let mut set = HashSet::new();
|
||||
set.insert(PathBuf::from("E:/wk-test-authorized"));
|
||||
let allowed = AllowedDirs { persistent: set, session: HashSet::new() };
|
||||
let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() };
|
||||
let outside = PathBuf::from("E:/wk-test-unauthorized/file.txt");
|
||||
assert!(!allowed.is_authorized(&outside), "未授权目录应被拒绝");
|
||||
}
|
||||
@@ -1007,7 +1035,7 @@ mod tests {
|
||||
fn test_allowed_dirs_session_authorized() {
|
||||
let mut session = HashSet::new();
|
||||
session.insert(PathBuf::from("E:/wk-temp-session"));
|
||||
let allowed = AllowedDirs { persistent: HashSet::new(), session };
|
||||
let allowed = AllowedDirs { persistent: HashSet::new(), session, once: HashSet::new() };
|
||||
assert!(allowed.is_authorized(&PathBuf::from("E:/wk-temp-session/file.txt")));
|
||||
}
|
||||
|
||||
@@ -1018,7 +1046,7 @@ mod tests {
|
||||
persistent.insert(PathBuf::from("E:/wk-persist"));
|
||||
let mut session = HashSet::new();
|
||||
session.insert(PathBuf::from("E:/wk-session"));
|
||||
let allowed = AllowedDirs { persistent, session };
|
||||
let allowed = AllowedDirs { persistent, session, once: HashSet::new() };
|
||||
assert!(allowed.is_authorized(&PathBuf::from("E:/wk-persist/a")));
|
||||
assert!(allowed.is_authorized(&PathBuf::from("E:/wk-session/b")));
|
||||
assert!(!allowed.is_authorized(&PathBuf::from("E:/wk-other/c")));
|
||||
@@ -1034,7 +1062,7 @@ mod tests {
|
||||
let mut persistent = HashSet::new();
|
||||
// 用户误把整个 C:\ 加入白名单
|
||||
persistent.insert(PathBuf::from("C:\\"));
|
||||
let allowed = AllowedDirs { persistent, session: HashSet::new() };
|
||||
let allowed = AllowedDirs { persistent, session: HashSet::new(), once: HashSet::new() };
|
||||
// System32 应被黑名单拒(尽管 C:\ 在白名单)
|
||||
assert!(!allowed.is_authorized(&PathBuf::from("C:\\Windows\\System32\\config\\sam")));
|
||||
}
|
||||
@@ -1044,7 +1072,7 @@ mod tests {
|
||||
fn test_blacklist_windows_program_files() {
|
||||
let mut persistent = HashSet::new();
|
||||
persistent.insert(PathBuf::from("C:\\"));
|
||||
let allowed = AllowedDirs { persistent, session: HashSet::new() };
|
||||
let allowed = AllowedDirs { persistent, session: HashSet::new(), once: HashSet::new() };
|
||||
assert!(!allowed.is_authorized(&PathBuf::from("C:\\Program Files\\SomeApp\\app.exe")));
|
||||
}
|
||||
|
||||
@@ -1100,7 +1128,7 @@ mod tests {
|
||||
fn test_check_path_session_hit() {
|
||||
let mut session = HashSet::new();
|
||||
session.insert(PathBuf::from("E:/wk-session"));
|
||||
let allowed = AllowedDirs { persistent: HashSet::new(), session };
|
||||
let allowed = AllowedDirs { persistent: HashSet::new(), session, once: HashSet::new() };
|
||||
match check_path_authorization("E:/wk-session/sub/file.txt", &allowed) {
|
||||
PathAuthDecision::Authorized => {}
|
||||
other => panic!("session 命中应 Authorized, got {:?}", other),
|
||||
|
||||
Reference in New Issue
Block a user