openlatch-client 0.1.18

OpenLatch runtime enforcement node — the capture-and-enforce client for the AI Operations Platform
//! Configuration plane monitoring — captures AI agent config-file changes,
//! hashes them, forwards to cloud via the existing cloud_tx rail.
//!
//! See `.local/brainstorms/config-plane-monitoring/PHASE-1-audit-and-inventory.md`
//! for the full design and `.claude/rules/config-plane-monitoring.md` for the
//! trust boundary, hash-pipeline invariants, and forbidden patterns.
//!
//! Module layout (compact 5-file split):
//! - `manifest.rs` — TOML parsing + path-template expansion
//! - `cache.rs` — bounded LRU `ContentHashCache`
//! - `watcher.rs` — `notify-debouncer-full` setup + ENOSPC PollWatcher fallback
//! - `monitor.rs` — main loop, hash pipeline, severity classifier, CloudEvent
//!   builder, initial-inventory walk, periodic rescan
//! - `mod.rs` — public API + `ConfigMonitor` entry point

pub mod alerts;
pub mod cache;
pub mod enrich;
pub mod manifest;
pub mod monitor;
pub mod watcher;

use std::sync::Arc;

use tokio::sync::mpsc;

pub use alerts::{run_long_poll, PendingAlert, PendingAlerts};
pub use cache::{CacheEntry, ContentHashCache};
pub use enrich::{
    enrich_session_start, try_register_project_from_cwd, DeclaredSource, EnrichError,
};
pub use manifest::{
    AgentManifest, AgentPath, ConfigScope, JsonSlicePath, Manifest, ManifestError, WatchStrategy,
};
pub use monitor::{ChangeKind, ConfigChangeRequest, EventSource, Severity};

use crate::cloud::CloudEvent;
use crate::config::Config;
use crate::core::logging::EventLogger;
use crate::privacy::PrivacyFilter;

/// Composes the manifest, watcher, monitor loop, and cache into a single
/// long-running daemon component.
pub struct ConfigMonitor {
    manifest: Arc<Manifest>,
    cache: Arc<ContentHashCache>,
    privacy_filter: PrivacyFilter,
    cloud_tx: Option<mpsc::Sender<CloudEvent>>,
    event_logger: EventLogger,
    config: Arc<Config>,
}

impl ConfigMonitor {
    /// Construct a `ConfigMonitor`. Does NOT start watching — call `spawn`.
    pub fn new(
        manifest: Arc<Manifest>,
        cache: Arc<ContentHashCache>,
        privacy_filter: PrivacyFilter,
        cloud_tx: Option<mpsc::Sender<CloudEvent>>,
        event_logger: EventLogger,
        config: Arc<Config>,
    ) -> Self {
        Self {
            manifest,
            cache,
            privacy_filter,
            cloud_tx,
            event_logger,
            config,
        }
    }

    /// Spawn the watcher + monitor loop. Returns a handle whose drop
    /// terminates the watchers; the spawned task is detached and lives for
    /// the daemon's lifetime.
    pub async fn spawn(self) -> anyhow::Result<ConfigMonitorHandle> {
        let (request_tx, request_rx) = mpsc::channel::<ConfigChangeRequest>(256);
        let watchers = watcher::spawn_watchers(
            &self.manifest,
            request_tx.clone(),
            self.config.inventory_monitor.watcher_debounce_ms,
        )?;
        let join = tokio::spawn(monitor::run(
            self.manifest,
            self.cache,
            self.privacy_filter,
            self.cloud_tx,
            self.event_logger,
            self.config,
            request_rx,
            request_tx.clone(),
        ));
        Ok(ConfigMonitorHandle {
            join,
            request_tx,
            _watchers: watchers,
        })
    }
}

/// Owned handle to a running `ConfigMonitor`. Held on the daemon's
/// `AppState` for the lifetime of the process. Drop terminates watchers;
/// the monitor loop exits naturally when the request channel closes.
pub struct ConfigMonitorHandle {
    /// JoinHandle for the monitor loop. Daemon shutdown drops this; the
    /// runtime cancels the task on drop. Field kept for future explicit
    /// shutdown wiring.
    #[allow(dead_code)]
    join: tokio::task::JoinHandle<()>,
    /// Sender for re-driving rescans / project-scope registers / native
    /// hook events.
    pub request_tx: mpsc::Sender<ConfigChangeRequest>,
    /// Held to keep watchers alive — they unwatch on drop.
    _watchers: Vec<watcher::WatcherGuard>,
}