feat: audit local agent file writes
- 为本地 agent 写入补充审计事件,区分原生修改与 mnote tool 写入 - 只读 grant 写入尝试会记录拒绝事件,便于会话面板追踪 changed files - 页面 AI smoke 脚本补充 changed files 展示链路验证 - 更新当前优先级 checklist 的完成状态与验证记录
This commit is contained in:
@@ -13,6 +13,7 @@ use futures_util::TryStreamExt;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Write;
|
||||
@@ -3752,7 +3753,12 @@ fn local_ai_session_root_dir(root_uri: &str) -> Result<PathBuf, WebError> {
|
||||
}
|
||||
|
||||
fn local_agent_audit_root_dir() -> PathBuf {
|
||||
PathBuf::from(LOCAL_AGENT_AUDIT_DIR)
|
||||
env::var("MNOTE_LOCAL_AGENT_AUDIT_DIR")
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from(LOCAL_AGENT_AUDIT_DIR))
|
||||
}
|
||||
|
||||
fn local_agent_audit_path() -> PathBuf {
|
||||
@@ -3990,6 +3996,7 @@ fn local_agent_audit_event(
|
||||
changed_files: Value,
|
||||
source_snapshot: Option<&LocalAgentAuditSnapshot>,
|
||||
_target_snapshot: Option<&LocalAgentAuditSnapshot>,
|
||||
write_attempt_rejected: bool,
|
||||
) -> Value {
|
||||
let root_uri = payload
|
||||
.get("rootUri")
|
||||
@@ -4014,7 +4021,6 @@ fn local_agent_audit_event(
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or(&context.auth.actor_type);
|
||||
let changed_file_count = changed_files.as_array().map(Vec::len).unwrap_or_default();
|
||||
let write_attempt_rejected = permission == "read" && changed_file_count > 0;
|
||||
json!({
|
||||
"eventId": format!("local_audit:{}:{}", sanitize_id_part(run_id), now_ms()),
|
||||
"actorId": actor_id,
|
||||
@@ -4065,6 +4071,8 @@ fn local_agent_audit_finalize_run(
|
||||
),
|
||||
(None, None) => Value::Array(vec![]),
|
||||
};
|
||||
let write_attempt_rejected = local_ai_payload_is_read_only(payload)
|
||||
&& changed_files.as_array().map(Vec::len).unwrap_or_default() > 0;
|
||||
let event = local_agent_audit_event(
|
||||
context,
|
||||
payload,
|
||||
@@ -4074,11 +4082,87 @@ fn local_agent_audit_finalize_run(
|
||||
changed_files,
|
||||
before.as_ref(),
|
||||
after.as_ref(),
|
||||
write_attempt_rejected,
|
||||
);
|
||||
local_agent_audit_write_event(&event)?;
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
pub(crate) fn local_agent_audit_record_write_rejected(
|
||||
context: &RequestContext,
|
||||
payload: Value,
|
||||
run_id: &str,
|
||||
acp_runtime: &str,
|
||||
) -> Result<Value, WebError> {
|
||||
let mut event = local_agent_audit_event(
|
||||
context,
|
||||
&payload,
|
||||
run_id,
|
||||
acp_runtime,
|
||||
"read_only_write_rejected",
|
||||
Value::Array(Vec::new()),
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
);
|
||||
if let Some(object) = event.as_object_mut() {
|
||||
object.insert(
|
||||
"toolName".into(),
|
||||
payload.get("toolName").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
object.insert(
|
||||
"toolCallId".into(),
|
||||
payload.get("toolCallId").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
object.insert(
|
||||
"rejection".into(),
|
||||
payload.get("rejection").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
}
|
||||
local_agent_audit_write_event(&event)?;
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
pub(crate) fn local_agent_audit_record_tool_write(
|
||||
context: &RequestContext,
|
||||
payload: Value,
|
||||
run_id: &str,
|
||||
acp_runtime: &str,
|
||||
) -> Result<Value, WebError> {
|
||||
let changed_files = payload
|
||||
.get("changedFiles")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Array(Vec::new()));
|
||||
let mut event = local_agent_audit_event(
|
||||
context,
|
||||
&payload,
|
||||
run_id,
|
||||
acp_runtime,
|
||||
"completed",
|
||||
changed_files,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
if let Some(object) = event.as_object_mut() {
|
||||
object.insert(
|
||||
"toolName".into(),
|
||||
payload.get("toolName").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
object.insert(
|
||||
"toolCallId".into(),
|
||||
payload.get("toolCallId").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
object.insert(
|
||||
"commandName".into(),
|
||||
payload.get("commandName").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
object.insert("origin".into(), Value::String("mnote_tool".into()));
|
||||
}
|
||||
local_agent_audit_write_event(&event)?;
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
fn local_ai_session_dir(root_uri: &str, share_id: Option<&str>) -> Result<PathBuf, WebError> {
|
||||
let root = local_ai_session_root_dir(root_uri)?;
|
||||
let mut dir = root.join("ai-sessions");
|
||||
|
||||
Reference in New Issue
Block a user