openlatch-client 0.1.18

OpenLatch runtime enforcement node — the capture-and-enforce client for the AI Operations Platform
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 TamperLoggerHandle {
    /// Wrap an already-spawned writer task, so the daemon can run the writer
    /// under its in-process task supervisor instead of a bare `tokio::spawn`.
    pub fn from_task(join_handle: tokio::task::JoinHandle<()>) -> Self {
        Self {
            _join_handle: join_handle,
        }
    }
}

impl TamperLogger {
    pub fn new(log_dir: PathBuf) -> (Self, TamperLoggerHandle) {
        let (logger, mut rx) = Self::channel();
        let handle = TamperLoggerHandle {
            _join_handle: tokio::spawn(async move { run_tamper_writer(log_dir, &mut rx).await }),
        };
        (logger, handle)
    }

    /// Sender + receiver without spawning the writer — see
    /// [`crate::core::logging::EventLogger::channel`] for why the spawn lives
    /// with the daemon rather than here.
    pub fn channel() -> (Self, mpsc::Receiver<TamperEvent>) {
        let (tx, rx) = mpsc::channel(256);
        (Self { tx }, rx)
    }

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

/// Drain tamper events onto `tamper.jsonl` until the channel closes.
///
/// `&mut` receiver so a supervisor can restart it on the same channel after a
/// panic without losing queued detections.
pub async fn run_tamper_writer(log_dir: PathBuf, rx: &mut 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");
            }
        }
    }
}