新增: 路径授权三档化 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:
2026-06-25 00:23:07 +08:00
parent 56a08fb5c1
commit 24c3fa08f1
10 changed files with 116 additions and 46 deletions

View File

@@ -751,9 +751,10 @@ fn append_cross_drive_note(result: serde_json::Value, note: &str) -> serde_json:
/// 触发:process_tool_calls 预校验文件工具路径未命中白名单 → 挂起 loop(insert pending
/// with path_auth=Some + emit AiDirAuthRequired)。前端弹窗三选项,用户选择后调本 IPC。
///
/// `decision`:
/// - `"once"`:写入 `AppState.allowed_dirs.session`(进程级会话临时授权,随 active 会话切换/新建/删除由 clear_session_allowed_dirs 清空)→ 执行工具 → 恢复 loop
/// - `"always"`:写入持久化 Settings KV `allowed_dirs`(reload_allowed_dirs 同步内存)→ 执行 → 恢复
/// `decision`(三授权档 + 拒):
/// - `"once"`:写入 `allowed_dirs.once`(本次单次,执行后 clear_once_allowed_dirs 清空)→ 执行 → 恢复
/// - `"session"`:写入 `allowed_dirs.session`(当前会话,切换/新建/删除活跃会话由 clear_session_allowed_dirs 清空)→ 执行 → 恢复
/// - `"always"`:写入持久化 Settings KV `allowed_dirs`(reload_allowed_dirs 同步内存 persistent)→ 执行 → 恢复
/// - `"deny"`:工具返 Err "用户拒绝路径授权" → 恢复 loop
///
/// 复用审批恢复架构:remove pending → (写授权/拒) → execute/Err → 替换占位 tool_result →
@@ -843,23 +844,33 @@ pub async fn ai_authorize_dir(
return Ok("rejected".to_string());
}
// ── "once":写会话级临时授权(随会话销毁,不落库) ──
// L1 补丁:遍历所有未授权父目录(rename_file from+to 不同目录时多项)。
if decision == "once" {
for d in &dir_strs {
state.add_session_allowed_dir(d.clone()).await;
// ── 授权档写入(三档:once 本次单次 / session 当前会话 / always 始终落 KV)──
// L1 补丁:遍历所有未授权父目录(rename_file from+to 不同目录时多项),确保 handler 放行
// deny 已在上方早 return,此处仅 once/session/always。
match decision.as_str() {
"once" => {
for d in &dir_strs {
state.add_once_allowed_dir(d.clone()).await;
}
tracing::info!(target: "ai_authz", dirs = ?dir_strs, conv_id = ?conv_id, "[AI-AUTHZ] 授权档=once(本次单次,执行后清)");
}
}
// ── "always":写持久化授权(Settings KV + 同步内存 persistent) ──
if decision == "always" {
for d in &dir_strs {
if let Err(e) = state.add_persistent_allowed_dir(d.clone()).await {
tracing::warn!("[F-03B] 持久化授权目录失败(降级仅本次会话): dir={} err={}", d, e);
// 降级:写入会话临时授权,保本路径本会话可用
"session" => {
for d in &dir_strs {
state.add_session_allowed_dir(d.clone()).await;
}
tracing::info!(target: "ai_authz", dirs = ?dir_strs, conv_id = ?conv_id, "[AI-AUTHZ] 授权档=session(当前会话,切会话清)");
}
"always" => {
for d in &dir_strs {
if let Err(e) = state.add_persistent_allowed_dir(d.clone()).await {
tracing::warn!("[F-03B] 持久化授权目录失败(降级当前会话): dir={} err={}", d, e);
// 降级:写入会话临时授权,保本路径本会话可用
state.add_session_allowed_dir(d.clone()).await;
}
}
tracing::info!(target: "ai_authz", dirs = ?dir_strs, conv_id = ?conv_id, "[AI-AUTHZ] 授权档=always(始终,落 KV)");
}
_ => {} // deny 已上方早 return;未知 decision 不写授权(执行会再走预校验弹窗)
}
// ── 执行工具(路径现已授权,handler 内 resolve_workspace_path_with_allowed 放行) ──
@@ -908,6 +919,10 @@ pub async fn ai_authorize_dir(
}
}
// once 授权档语义=本次单次:工具执行完(handler 已读 once 放行)立即清空,下次同路径仍弹窗。
// 仅 once 档写入 once 层,无条件清空安全(session/always 不写 once,清空无副作用)。
state.clear_once_allowed_dirs().await;
// 替换占位 tool_result 为真实结果
{
let mut session = state.ai_session.lock().await;