use std::path::{Path, PathBuf};
use crate::error::OlError;
use super::{Supervisor, SupervisorKind, SupervisorStatus, ERR_SUPERVISION_INSTALL_FAILED};
const SERVICE_NAME: &str = "openlatch.service";
pub fn is_systemd_available() -> bool {
Path::new("/run/systemd/system").exists()
}
fn run_systemctl(args: &[&str]) -> Result<(), OlError> {
let out = std::process::Command::new("systemctl").args(args).output();
match out {
Ok(o) if o.status.success() => Ok(()),
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr).trim().to_string();
Err(OlError::new(
ERR_SUPERVISION_INSTALL_FAILED,
format!("systemctl {} failed: {}", args.join(" "), stderr),
))
}
Err(e) => Err(OlError::new(
ERR_SUPERVISION_INSTALL_FAILED,
format!("Cannot run systemctl: {e}"),
)),
}
}
pub struct SystemdSupervisor {
unit_path: PathBuf,
}
impl Default for SystemdSupervisor {
fn default() -> Self {
Self::new()
}
}
impl SystemdSupervisor {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
Self {
unit_path: home.join(".config/systemd/user").join(SERVICE_NAME),
}
}
fn generate_unit(&self, binary_path: &Path) -> String {
let bin = binary_path.display();
let home = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/home/user"))
.display()
.to_string();
format!(
r#"[Unit]
Description=OpenLatch AI Agent Security Daemon
After=network.target
[Service]
Type=simple
ExecStart={bin} daemon start --foreground
Restart=on-failure
RestartSec=10
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths={home}/.openlatch {home}/.claude
[Install]
WantedBy=default.target
"#
)
}
}
impl Supervisor for SystemdSupervisor {
fn kind(&self) -> SupervisorKind {
SupervisorKind::Systemd
}
fn install(&self, binary_path: &Path) -> Result<(), OlError> {
if let Some(parent) = self.unit_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
OlError::new(
ERR_SUPERVISION_INSTALL_FAILED,
format!("Cannot create systemd user directory: {e}"),
)
})?;
}
let unit = self.generate_unit(binary_path);
std::fs::write(&self.unit_path, &unit).map_err(|e| {
OlError::new(
ERR_SUPERVISION_INSTALL_FAILED,
format!("Cannot write systemd unit file: {e}"),
)
})?;
run_systemctl(&["--user", "daemon-reload"])?;
run_systemctl(&["--user", "enable", "--now", "openlatch.service"])?;
Ok(())
}
fn uninstall(&self) -> Result<(), OlError> {
let _ = std::process::Command::new("systemctl")
.args(["--user", "stop", "openlatch.service"])
.output();
let _ = std::process::Command::new("systemctl")
.args(["--user", "disable", "openlatch.service"])
.output();
let _ = std::fs::remove_file(&self.unit_path);
let _ = std::process::Command::new("systemctl")
.args(["--user", "daemon-reload"])
.output();
Ok(())
}
fn status(&self) -> Result<SupervisorStatus, OlError> {
if !self.unit_path.exists() {
return Ok(SupervisorStatus {
installed: false,
running: false,
description: "not installed".into(),
});
}
let output = std::process::Command::new("systemctl")
.args(["--user", "is-active", "openlatch.service"])
.output();
let running = output.as_ref().is_ok_and(|o| o.status.success());
Ok(SupervisorStatus {
installed: true,
running,
description: if running {
"systemd-user (Restart=on-failure active)".into()
} else {
"systemd-user (unit present, not running)".into()
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_file_has_restart_on_failure() {
let sup = SystemdSupervisor::new();
let unit = sup.generate_unit(Path::new("/opt/openlatch/bin/openlatch"));
assert!(unit.contains("Restart=on-failure"));
}
#[test]
fn unit_file_has_sandboxing() {
let sup = SystemdSupervisor::new();
let unit = sup.generate_unit(Path::new("/opt/openlatch/bin/openlatch"));
assert!(unit.contains("NoNewPrivileges=true"));
assert!(unit.contains("ProtectSystem=strict"));
assert!(unit.contains("ProtectHome=read-only"));
}
#[test]
fn unit_file_has_read_write_paths() {
let sup = SystemdSupervisor::new();
let unit = sup.generate_unit(Path::new("/opt/openlatch/bin/openlatch"));
assert!(unit.contains("ReadWritePaths="));
assert!(unit.contains(".openlatch"));
assert!(unit.contains(".claude"));
}
}