From 90798b936ec925ea4927d4e55b4179dabfedae3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Thu, 14 May 2026 20:51:35 +0200 Subject: [PATCH] hive-c0re: nixos-container lifecycle (spawn/kill/rebuild/list) --- hive-c0re/src/lifecycle.rs | 60 ++++++++++++++++++++++++++++++++++++++ hive-c0re/src/main.rs | 1 + hive-c0re/src/server.rs | 42 ++++++++++++++++---------- 3 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 hive-c0re/src/lifecycle.rs diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs new file mode 100644 index 0000000..60f1f71 --- /dev/null +++ b/hive-c0re/src/lifecycle.rs @@ -0,0 +1,60 @@ +//! Thin async wrappers over `nixos-container`. + +use anyhow::{Context, Result, bail}; +use tokio::process::Command; + +pub const AGENT_PREFIX: &str = "hive-agent-"; +pub const HIVE_PREFIX: &str = "hive-"; + +pub fn container_name(name: &str) -> String { + format!("{AGENT_PREFIX}{name}") +} + +pub async fn spawn(name: &str, agent_flake: &str) -> Result<()> { + let container = container_name(name); + run(&["create", &container, "--flake", agent_flake]).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(()) +} diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 0f13e81..c84305b 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -5,6 +5,7 @@ use clap::{Parser, Subcommand}; use hive_sh4re::{HostRequest, HostResponse}; mod client; +mod lifecycle; mod server; #[derive(Parser)] diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index d7b9fd3..48eeb56 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -5,6 +5,8 @@ use hive_sh4re::{HostRequest, HostResponse}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; +use crate::lifecycle; + pub async fn serve(socket: &Path, agent_flake: &str) -> Result<()> { if let Some(parent) = socket.parent() { std::fs::create_dir_all(parent) @@ -51,20 +53,30 @@ async fn handle(stream: UnixStream, agent_flake: &str) -> Result<()> { } } -async fn dispatch(req: &HostRequest, _agent_flake: &str) -> HostResponse { - match req { - HostRequest::Spawn { name } => { - tracing::info!(%name, "spawn (stub)"); - HostResponse::error("spawn: nixos-container integration pending") - } - HostRequest::Kill { name } => { - tracing::info!(%name, "kill (stub)"); - HostResponse::error("kill: nixos-container integration pending") - } - HostRequest::Rebuild { name } => { - tracing::info!(%name, "rebuild (stub)"); - HostResponse::error("rebuild: nixos-container integration pending") - } - HostRequest::List => HostResponse::list(Vec::new()), +async fn dispatch(req: &HostRequest, agent_flake: &str) -> HostResponse { + let result: anyhow::Result = async { + Ok(match req { + HostRequest::Spawn { name } => { + tracing::info!(%name, "spawn"); + lifecycle::spawn(name, agent_flake).await?; + HostResponse::success() + } + HostRequest::Kill { name } => { + tracing::info!(%name, "kill"); + lifecycle::kill(name).await?; + HostResponse::success() + } + HostRequest::Rebuild { name } => { + tracing::info!(%name, "rebuild"); + lifecycle::rebuild(name, agent_flake).await?; + HostResponse::success() + } + HostRequest::List => HostResponse::list(lifecycle::list().await?), + }) + } + .await; + match result { + Ok(r) => r, + Err(e) => HostResponse::error(format!("{e:#}")), } }