use std::path::{Path, PathBuf};
use crate::error::OlError;
use super::{
Supervisor, SupervisorKind, SupervisorStatus, ERR_SUPERVISION_CONTROL_FAILED,
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();
let marker = super::unit_version_marker();
format!(
r#"[Unit]
Description=OpenLatch runtime enforcement node
Documentation=https://docs.openlatch.ai
After=network.target
# {marker}
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStart={bin} daemon start --foreground
Restart=always
RestartSec=2
RestartPreventExitStatus=5
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,
unit_current: 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());
let state = output
.as_ref()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown".into());
let unit_current = std::fs::read_to_string(&self.unit_path)
.map(|c| super::unit_is_current(&c))
.unwrap_or(false);
Ok(SupervisorStatus {
installed: true,
running,
unit_current,
description: match (running, unit_current) {
(true, true) => "systemd-user (Restart=always active)".into(),
(true, false) => {
"systemd-user (running an outdated unit — reinstall to get Restart=always)"
.into()
}
(false, _) => format!("systemd-user (unit present, state: {state})"),
},
})
}
fn start(&self) -> Result<(), OlError> {
run_control(&["--user", "start", SERVICE_NAME])
}
fn stop(&self) -> Result<(), OlError> {
run_control(&["--user", "stop", SERVICE_NAME])
}
fn restart(&self) -> Result<(), OlError> {
run_control(&["--user", "restart", SERVICE_NAME])
}
}
fn run_control(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) => Err(OlError::new(
ERR_SUPERVISION_CONTROL_FAILED,
format!(
"systemctl {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&o.stderr).trim()
),
)),
Err(e) => Err(OlError::new(
ERR_SUPERVISION_CONTROL_FAILED,
format!("Cannot run systemctl: {e}"),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_file_restarts_always_and_never_gives_up() {
let sup = SystemdSupervisor::new();
let unit = sup.generate_unit(Path::new("/opt/openlatch/bin/openlatch"));
assert!(unit.contains("Restart=always"), "unit:\n{unit}");
assert!(!unit.contains("Restart=on-failure"), "unit:\n{unit}");
assert!(unit.contains("RestartSec=2"), "unit:\n{unit}");
assert!(unit.contains("StartLimitIntervalSec=0"), "unit:\n{unit}");
}
#[test]
fn unit_file_does_not_restart_the_already_running_refusal() {
let sup = SystemdSupervisor::new();
let unit = sup.generate_unit(Path::new("/opt/openlatch/bin/openlatch"));
assert!(
unit.contains("RestartPreventExitStatus=5"),
"exit 5 (OL-1501) must not be restarted:\n{unit}"
);
assert!(
!unit.contains("RestartPreventExitStatus=1"),
"exempting exit 1 would stop restarts on genuine crashes:\n{unit}"
);
}
#[test]
fn unit_file_carries_the_version_marker() {
let sup = SystemdSupervisor::new();
let unit = sup.generate_unit(Path::new("/opt/openlatch/bin/openlatch"));
assert!(
super::super::unit_is_current(&unit),
"generated unit must carry the current marker:\n{unit}"
);
assert!(!super::super::unit_is_current(
"[Service]\nRestart=on-failure\n"
));
}
#[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"));
}
#[test]
fn read_write_paths_never_cover_the_unit_directory() {
let sup = SystemdSupervisor::new();
let unit = sup.generate_unit(Path::new("/opt/openlatch/bin/openlatch"));
let rw = unit
.lines()
.find(|l| l.starts_with("ReadWritePaths="))
.expect("unit must declare ReadWritePaths");
for spelling in [
".config/systemd/user",
"%E/systemd/user",
".local/share/systemd/user",
"%D/systemd/user",
] {
assert!(
!rw.contains(spelling),
"ReadWritePaths must never cover a unit directory (matched '{spelling}') — \
that would let a compromised daemon rewrite its own startup unit. Migration \
belongs in the CLI (see migrate_supervisor_artifact_if_stale). Got: {rw}"
);
}
}
}