openlatch-client 0.1.14

The open-source security layer for AI agents — client forwarder
pub mod launchd;
pub mod systemd;
pub mod task_scheduler;

use crate::error::OlError;

pub const ERR_SUPERVISION_UNSUPPORTED_OS: &str = "OL-1950";
pub const ERR_SUPERVISION_INSTALL_FAILED: &str = "OL-1951";
pub const ERR_SUPERVISION_BOOTSTRAP_FAILED: &str = "OL-1952";

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SupervisorKind {
    Launchd,
    Systemd,
    TaskScheduler,
    None,
}

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SupervisionMode {
    Active,
    Deferred,
    Disabled,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SupervisionConfig {
    pub mode: SupervisionMode,
    pub backend: SupervisorKind,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub disabled_reason: Option<String>,
}

impl Default for SupervisionConfig {
    fn default() -> Self {
        Self {
            mode: SupervisionMode::Disabled,
            backend: SupervisorKind::None,
            disabled_reason: Some("not_initialized".into()),
        }
    }
}

pub trait Supervisor: Send + Sync {
    fn kind(&self) -> SupervisorKind;
    fn install(&self, binary_path: &std::path::Path) -> Result<(), OlError>;
    fn uninstall(&self) -> Result<(), OlError>;
    fn status(&self) -> Result<SupervisorStatus, OlError>;
}

#[derive(Debug, Clone)]
pub struct SupervisorStatus {
    pub installed: bool,
    pub running: bool,
    pub description: String,
}

pub fn select_supervisor() -> Option<Box<dyn Supervisor>> {
    select_supervisor_impl()
}

#[cfg(target_os = "macos")]
fn select_supervisor_impl() -> Option<Box<dyn Supervisor>> {
    Some(Box::new(launchd::LaunchdSupervisor::new()))
}

#[cfg(target_os = "linux")]
fn select_supervisor_impl() -> Option<Box<dyn Supervisor>> {
    if systemd::is_systemd_available() {
        Some(Box::new(systemd::SystemdSupervisor::new()))
    } else {
        None
    }
}

#[cfg(target_os = "windows")]
fn select_supervisor_impl() -> Option<Box<dyn Supervisor>> {
    Some(Box::new(task_scheduler::TaskSchedulerSupervisor::new()))
}

#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn select_supervisor_impl() -> Option<Box<dyn Supervisor>> {
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_supervision_config_is_disabled() {
        let c = SupervisionConfig::default();
        assert_eq!(c.mode, SupervisionMode::Disabled);
        assert_eq!(c.backend, SupervisorKind::None);
    }

    #[test]
    fn select_supervisor_returns_something_on_this_os() {
        let sup = select_supervisor();
        #[cfg(any(target_os = "macos", target_os = "windows"))]
        assert!(sup.is_some());
        let _ = sup;
    }
}