//! Thin async wrappers over `nixos-container`. use std::path::Path; use anyhow::{Context, Result, bail}; use tokio::process::Command; pub const AGENT_PREFIX: &str = "hive-agent-"; pub const HIVE_PREFIX: &str = "hive-"; /// Mount point of the per-agent runtime directory inside the container. pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive"; pub fn container_name(name: &str) -> String { format!("{AGENT_PREFIX}{name}") } pub async fn spawn(name: &str, agent_flake: &str, agent_dir: &Path) -> Result<()> { let container = container_name(name); let bind = format!("{}:{CONTAINER_RUNTIME_MOUNT}", agent_dir.display()); run(&["create", &container, "--flake", agent_flake, "--bind", &bind]).await?; run(&["start", &container]).await } pub async fn kill(name: &str) -> Result<()> { let container = container_name(name); run(&["stop", &container]).await } pub async fn rebuild(name: &str, agent_flake: &str) -> Result<()> { let container = container_name(name); run(&["update", &container, "--flake", agent_flake]).await } pub async fn list() -> Result> { let out = Command::new("nixos-container") .arg("list") .output() .await .context("invoke nixos-container list")?; if !out.status.success() { bail!( "nixos-container list exited with status {}: {}", out.status, String::from_utf8_lossy(&out.stderr).trim() ); } Ok(String::from_utf8_lossy(&out.stdout) .lines() .map(str::trim) .filter(|line| line.starts_with(HIVE_PREFIX)) .map(str::to_owned) .collect()) } async fn run(args: &[&str]) -> Result<()> { let status = Command::new("nixos-container") .args(args) .status() .await .with_context(|| format!("invoke nixos-container {}", args.join(" ")))?; if !status.success() { bail!("nixos-container {} exited with {status}", args.join(" ")); } Ok(()) }