openlatch-client 0.1.14

The open-source security layer for AI agents — client forwarder
use std::path::PathBuf;

use tokio::sync::mpsc;

use super::super::cloud::tamper::TamperEvent;

#[derive(Clone)]
pub struct TamperLogger {
    tx: mpsc::Sender<TamperEvent>,
}

pub struct TamperLoggerHandle {
    _join_handle: tokio::task::JoinHandle<()>,
}

impl TamperLogger {
    pub fn new(log_dir: PathBuf) -> (Self, TamperLoggerHandle) {
        let (tx, rx) = mpsc::channel(256);
        let handle = TamperLoggerHandle {
            _join_handle: tokio::spawn(writer_task(log_dir, rx)),
        };
        (Self { tx }, handle)
    }

    pub fn log(&self, event: TamperEvent) {
        if self.tx.try_send(event).is_err() {
            tracing::warn!("tamper log channel full or closed, event dropped");
        }
    }
}

async fn writer_task(log_dir: PathBuf, mut rx: mpsc::Receiver<TamperEvent>) {
    let path = log_dir.join("tamper.jsonl");
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }

    while let Some(event) = rx.recv().await {
        match serde_json::to_string(&event) {
            Ok(json_line) => {
                use std::io::Write;
                let mut file = match std::fs::OpenOptions::new()
                    .create(true)
                    .append(true)
                    .open(&path)
                {
                    Ok(f) => f,
                    Err(e) => {
                        tracing::warn!(error = %e, "cannot open tamper.jsonl");
                        continue;
                    }
                };
                if let Err(e) = writeln!(file, "{json_line}") {
                    tracing::warn!(error = %e, "cannot write to tamper.jsonl");
                }
            }
            Err(e) => {
                tracing::warn!(error = %e, "cannot serialize tamper event");
            }
        }
    }
}