#![allow(dead_code, unused_imports, unused_variables)]
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub timestamp: DateTime<Utc>,
pub session_id: String,
pub event_type: AuditEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args_hash: Option<String>,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_decision: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditEventType {
ToolExecution,
SafetyBlock,
UserSkip,
SessionStart,
SessionEnd,
PermissionGrant,
HookExecution,
}
pub struct AuditLogger {
tx: mpsc::UnboundedSender<AuditEvent>,
session_id: String,
}
impl AuditLogger {
pub fn new(session_id: &str) -> Option<Self> {
let audit_dir = Self::audit_dir()?;
let (tx, rx) = mpsc::unbounded_channel();
let dir = audit_dir.clone();
tokio::spawn(async move {
Self::writer_loop(rx, dir).await;
});
info!("Audit logging enabled at {:?}", audit_dir);
Some(Self {
tx,
session_id: session_id.to_string(),
})
}
fn audit_dir() -> Option<PathBuf> {
let dir = dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("selfware")
.join("audit");
if let Err(e) = std::fs::create_dir_all(&dir) {
warn!("Failed to create audit directory {:?}: {}", dir, e);
return None;
}
Some(dir)
}
async fn writer_loop(mut rx: mpsc::UnboundedReceiver<AuditEvent>, audit_dir: PathBuf) {
use tokio::io::AsyncWriteExt;
let mut current_date = String::new();
let mut file: Option<tokio::fs::File> = None;
while let Some(event) = rx.recv().await {
let date = event.timestamp.format("%Y-%m-%d").to_string();
if date != current_date {
current_date = date.clone();
let path = audit_dir.join(format!("{}.jsonl", date));
match tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.await
{
Ok(f) => file = Some(f),
Err(e) => {
warn!("Failed to open audit file {:?}: {}", path, e);
continue;
}
}
}
if let Some(ref mut f) = file {
if let Ok(mut line) = serde_json::to_vec(&event) {
line.push(b'\n');
if let Err(e) = f.write_all(&line).await {
warn!("Failed to write audit event: {}", e);
}
}
}
}
debug!("Audit writer loop exited");
}
pub fn log_tool_execution(
&self,
tool_name: &str,
args_hash: &str,
success: bool,
duration_ms: u64,
user_decision: Option<&str>,
) {
let _ = self.tx.send(AuditEvent {
timestamp: Utc::now(),
session_id: self.session_id.clone(),
event_type: AuditEventType::ToolExecution,
tool_name: Some(tool_name.to_string()),
args_hash: Some(args_hash.to_string()),
success,
duration_ms: Some(duration_ms),
user_decision: user_decision.map(String::from),
context: None,
});
}
pub fn log_safety_block(&self, tool_name: &str, reason: &str) {
let _ = self.tx.send(AuditEvent {
timestamp: Utc::now(),
session_id: self.session_id.clone(),
event_type: AuditEventType::SafetyBlock,
tool_name: Some(tool_name.to_string()),
args_hash: None,
success: false,
duration_ms: None,
user_decision: None,
context: Some(reason.to_string()),
});
}
pub fn log_session_start(&self) {
let _ = self.tx.send(AuditEvent {
timestamp: Utc::now(),
session_id: self.session_id.clone(),
event_type: AuditEventType::SessionStart,
tool_name: None,
args_hash: None,
success: true,
duration_ms: None,
user_decision: None,
context: None,
});
}
pub fn log_session_end(&self) {
let _ = self.tx.send(AuditEvent {
timestamp: Utc::now(),
session_id: self.session_id.clone(),
event_type: AuditEventType::SessionEnd,
tool_name: None,
args_hash: None,
success: true,
duration_ms: None,
user_decision: None,
context: None,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_event_serialization() {
let event = AuditEvent {
timestamp: Utc::now(),
session_id: "test-123".to_string(),
event_type: AuditEventType::ToolExecution,
tool_name: Some("file_write".to_string()),
args_hash: Some("abc123".to_string()),
success: true,
duration_ms: Some(42),
user_decision: Some("auto-approved".to_string()),
context: None,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"tool_execution\""));
assert!(json.contains("\"file_write\""));
assert!(!json.contains("\"context\"")); }
#[test]
fn test_audit_event_types() {
let types = vec![
AuditEventType::ToolExecution,
AuditEventType::SafetyBlock,
AuditEventType::UserSkip,
AuditEventType::SessionStart,
AuditEventType::SessionEnd,
];
for t in types {
let json = serde_json::to_string(&t).unwrap();
assert!(!json.is_empty());
}
}
}