From 68cc433ac996a6bbeb589c55a77e1ccbc564c424 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 1 Jun 2026 18:27:11 +0200 Subject: [PATCH] fix(#977): add MANAGER_CONTAINER=h-root, migrate root container name --- docs/agent-hierarchy.md | 19 ++++--- hive-c0re/src/agent_sockets.rs | 3 +- hive-c0re/src/container_view.rs | 4 +- hive-c0re/src/lifecycle.rs | 28 +++++----- hive-c0re/src/migrate.rs | 90 +++++++++++++++++++++++++++++++-- 5 files changed, 115 insertions(+), 29 deletions(-) diff --git a/docs/agent-hierarchy.md b/docs/agent-hierarchy.md index b3bb5b47..6accf7c3 100644 --- a/docs/agent-hierarchy.md +++ b/docs/agent-hierarchy.md @@ -104,13 +104,15 @@ which axis the post-milestone version reads each special-case along: ### A — naming + bootstrap -- `MANAGER_AGENT = "root"` (broker recipient name) and - `MANAGER_NAME = "root"` (container name). Both now `"root"`; - renamed from `"manager"`/`"hm1nd"` via the migration script in - `migrate.rs` (idempotent, marker-guarded). +- `MANAGER_AGENT = "root"` (broker recipient name), + `MANAGER_NAME = "root"` (logical name, state-dir key), and + `MANAGER_CONTAINER = "h-root"` (nixos-container name). The `h-` + prefix makes the manager consistent with all sub-agents and lets + `lifecycle::list()` use a single `starts_with("h-")` filter. + Existing `root` containers are renamed to `h-root` by migration + phase 5 in `migrate.rs` (idempotent, marker-guarded). - `auto_update::ensure_manager` runs at hive-c0re boot and spawns - `root` if missing. Becomes "ensure the root agent exists" once any - agent can be at the root. **Topology**: root has no parent, so + `h-root` if missing. **Topology**: root has no parent, so hive-c0re itself owns its lifecycle (no parent to delegate to). ### B — wire-protocol privileges @@ -236,8 +238,9 @@ repos they're working on. meta-flake's per-agent flake.nix wrapper). For the manager unit that means a hardcoded `HIVE_LABEL` env value: -- `HIVE_LABEL = "root"` — container name; matches what `meta.rs` - injects at deploy time. +- `HIVE_LABEL = "root"` — logical agent name; matches what `meta.rs` + injects at deploy time. (The nixos-container is `h-root`, but the + harness identifies itself by logical name.) Real deploys never read these — `meta::render_flake` overrides them via the generated wrapper. They exist so the manager diff --git a/hive-c0re/src/agent_sockets.rs b/hive-c0re/src/agent_sockets.rs index f31f5b28..d7edbfd6 100644 --- a/hive-c0re/src/agent_sockets.rs +++ b/hive-c0re/src/agent_sockets.rs @@ -13,8 +13,6 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; -use crate::lifecycle::MANAGER_NAME; - const HOST_SOCKETS_PATH: &str = "/var/lib/hyperhive/agent-sockets.json"; /// Host-side parent directory holding per-agent socket subdirs. The @@ -217,6 +215,7 @@ pub fn spawn_poll() { #[cfg(test)] mod tests { use super::*; + use crate::lifecycle::MANAGER_NAME; #[test] fn socket_path_for_uses_subdir_layout() { diff --git a/hive-c0re/src/container_view.rs b/hive-c0re/src/container_view.rs index a01b1330..17c29fca 100644 --- a/hive-c0re/src/container_view.rs +++ b/hive-c0re/src/container_view.rs @@ -12,7 +12,7 @@ use rusqlite::Connection; use serde::{Deserialize, Serialize}; use crate::coordinator::Coordinator; -use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME}; +use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_CONTAINER, MANAGER_NAME}; /// An agent-declared extra navigation link surfaced on the dashboard card. /// Written by the `hive-dashboard-links` NixOS oneshot into @@ -108,7 +108,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec { let topology = crate::topology::read(); let mut out = Vec::new(); for c in &raw { - let (logical, is_manager) = if c == MANAGER_NAME { + let (logical, is_manager) = if c == MANAGER_CONTAINER { (MANAGER_NAME.to_owned(), true) } else if let Some(n) = c.strip_prefix(AGENT_PREFIX) { (n.to_owned(), false) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index cc834ae0..4e28c1a5 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -10,11 +10,14 @@ use tokio::process::Command; /// name itself can be at most `MAX_AGENT_NAME` chars. pub const AGENT_PREFIX: &str = "h-"; pub const MAX_AGENT_NAME: usize = 9; -/// Container name of the manager. Lives in the same path scheme as sub-agents -/// (`/var/lib/hyperhive/agents/root/`, `/var/lib/hyperhive/applied/root/`), -/// but its container has no `h-` prefix and extends a different -/// nixosConfiguration (`root`, not `agent-base`). +/// Logical name of the manager agent (broker recipient, state-dir key, +/// meta flake attribute). All persistent state lives under `root/`. pub const MANAGER_NAME: &str = "root"; +/// Container name of the manager. Uses the same `h-` prefix as sub-agents +/// so `nixos-container list` output is uniform and the list filter is +/// a single `starts_with(AGENT_PREFIX)` check. Logical name → container +/// name: `root` → `h-root`. +pub const MANAGER_CONTAINER: &str = "h-root"; /// Mount point of the per-agent runtime directory inside the container. pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive"; @@ -72,7 +75,7 @@ pub fn agent_web_port(name: &str) -> u16 { #[must_use] pub fn container_name(name: &str) -> String { if name == MANAGER_NAME { - MANAGER_NAME.to_owned() + MANAGER_CONTAINER.to_owned() } else { format!("{AGENT_PREFIX}{name}") } @@ -161,7 +164,7 @@ async fn port_collision(self_name: &str) -> Option { let port = agent_web_port(self_name); let raw = list().await.unwrap_or_default(); for c in raw { - let other = if c == MANAGER_NAME { + let other = if c == MANAGER_CONTAINER { MANAGER_NAME.to_owned() } else if let Some(n) = c.strip_prefix(AGENT_PREFIX) { n.to_owned() @@ -233,7 +236,7 @@ async fn agents_for_meta(name_to_add: Option<&str>) -> Result = containers .into_iter() .filter_map(|c| { - let (name, is_manager) = if c == MANAGER_NAME { + let (name, is_manager) = if c == MANAGER_CONTAINER { (MANAGER_NAME.to_owned(), true) } else if let Some(n) = c.strip_prefix(AGENT_PREFIX) { (n.to_owned(), false) @@ -598,7 +601,7 @@ pub async fn list() -> Result> { Ok(String::from_utf8_lossy(&out.stdout) .lines() .map(str::trim) - .filter(|line| line.starts_with(AGENT_PREFIX) || *line == MANAGER_NAME) + .filter(|line| line.starts_with(AGENT_PREFIX)) .map(str::to_owned) .collect()) } @@ -1076,9 +1079,8 @@ fn set_nspawn_flags( let path = format!("/etc/nixos-containers/{container}.conf"); let original = std::fs::read_to_string(&path).with_context(|| format!("read {path}"))?; - // Logical agent name (container name minus the sub-agent prefix). - // For the manager the strip is a no-op — harmless, manager paths - // below are gated on `container == MANAGER_NAME` anyway. + // Logical agent name — strip the `h-` prefix. + // For the manager: `h-root` → `root`. For sub-agents: `h-iris` → `iris`. let agent_name = container.strip_prefix(AGENT_PREFIX).unwrap_or(container); // Claude credentials land at `/home//.claude` so the @@ -1099,7 +1101,7 @@ fn set_nspawn_flags( // the `/agents` bind below already exposes both (along with // every sub-agent's). For regular agents the harness dir is // the sibling of notes_dir (same parent, "harness" subdir). - if container != MANAGER_NAME { + if container != MANAGER_CONTAINER { let _ = write!( binds, " --bind={notes}:/agents/{agent_name}/state", @@ -1120,7 +1122,7 @@ fn set_nspawn_flags( ); } } - if container == MANAGER_NAME { + if container == MANAGER_CONTAINER { // systemd-nspawn refuses to start a container whose bind // source doesn't exist. The meta repo is created by the // startup migration, but make sure the directory is there diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index e2c4336c..044b572a 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -1,5 +1,5 @@ -//! Startup auto-migration from the pre-meta layout. Four idempotent -//! phases: applied repo, proposed repo, meta repo, container repoint. +//! Startup auto-migration. Five idempotent phases: applied repo, +//! proposed repo, meta repo, container repoint, root→h-root rename. //! Kill-switch: `HIVE_SKIP_META_MIGRATION=1`. Full migration sequence //! and phase details: `docs/approvals.md::Migration from the pre-tag`. @@ -10,7 +10,7 @@ use anyhow::{Context, Result}; use tokio::process::Command; use crate::coordinator::Coordinator; -use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME}; +use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_CONTAINER, MANAGER_NAME}; use crate::meta; const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION"; @@ -21,6 +21,12 @@ fn repoint_marker() -> PathBuf { PathBuf::from("/var/lib/hyperhive/.meta-migration-done") } +/// Marker for phase 5. Once present, root→h-root container rename is +/// skipped on future restarts. +fn hroot_rename_marker() -> PathBuf { + PathBuf::from("/var/lib/hyperhive/.hroot-rename-done") +} + /// Substring that identifies the *current* agent flake boilerplate. /// Bumped whenever the template changes so the startup migration /// re-renders existing agents onto the new shape. Today the marker @@ -108,6 +114,12 @@ pub async fn run(coord: &Arc) -> Result<()> { { tracing::warn!(error = ?e, "migration: write repoint marker failed"); } + + // Phase 5: rename `root` nixos-container to `h-root` for naming + // consistency with sub-agents. Guarded by marker; skipped on + // fresh installs (conf file absent) and after first successful run. + rename_manager_container(coord).await; + Ok(()) } @@ -140,12 +152,82 @@ fn migrate_harness_files(name: &str) { } } +/// Phase 5: rename the `root` nixos-container to `h-root` so the +/// manager container name is consistent with the `h-` prefix used by +/// all sub-agents. Idempotent and marker-guarded. Steps: +/// +/// 1. Check `/etc/nixos-containers/root.conf` exists (old name present). +/// 2. Stop the `root` container. +/// 3. Copy `root.conf` → `h-root.conf`. +/// 4. Move `/var/lib/nixos-containers/root/` → `h-root/` (if present). +/// 5. `systemctl daemon-reload` so systemd sees the new unit name. +/// 6. `nixos-container start h-root`. +/// 7. Write the done marker. +/// +/// Best-effort: logs warnings on failure. A failed rename leaves both +/// conf files present; on the next hive-c0re start the marker is +/// absent so the phase retries. +async fn rename_manager_container(coord: &Arc) { + if hroot_rename_marker().exists() { + return; + } + let old_conf = std::path::PathBuf::from("/etc/nixos-containers/root.conf"); + let new_conf = std::path::PathBuf::from("/etc/nixos-containers/h-root.conf"); + if !old_conf.exists() { + // Fresh install — root container was never created under the old name. + let _ = std::fs::write(hroot_rename_marker(), b"done\n"); + return; + } + if new_conf.exists() { + // Already renamed (but marker was lost — write it and return). + tracing::info!("migration phase 5: h-root.conf already present, marking done"); + let _ = std::fs::write(hroot_rename_marker(), b"done\n"); + return; + } + tracing::info!("migration phase 5: renaming root container to h-root"); + let _guard = coord.transient_guard(MANAGER_NAME, crate::coordinator::TransientKind::Rebuilding); + + // Stop the old container. + if let Err(e) = Command::new("nixos-container").args(["stop", "root"]).status().await { + tracing::warn!(error = ?e, "migration phase 5: nixos-container stop root failed"); + } + + // Copy conf file. + if let Err(e) = std::fs::copy(&old_conf, &new_conf) { + tracing::warn!(error = ?e, "migration phase 5: copy root.conf failed — aborting"); + return; + } + + // Move rootfs if it exists (may be absent for ephemeral containers). + let old_rootfs = std::path::PathBuf::from("/var/lib/nixos-containers/root"); + let new_rootfs = std::path::PathBuf::from("/var/lib/nixos-containers/h-root"); + if old_rootfs.exists() && !new_rootfs.exists() { + if let Err(e) = std::fs::rename(&old_rootfs, &new_rootfs) { + tracing::warn!(error = ?e, "migration phase 5: rename rootfs failed (non-fatal)"); + } + } + + // Daemon reload so systemd picks up the new container@h-root unit. + if let Err(e) = Command::new("systemctl").args(["daemon-reload"]).status().await { + tracing::warn!(error = ?e, "migration phase 5: systemctl daemon-reload failed"); + } + + // Start the renamed container. + if let Err(e) = Command::new("nixos-container").args(["start", "h-root"]).status().await { + tracing::warn!(error = ?e, "migration phase 5: nixos-container start h-root failed"); + return; + } + + tracing::info!("migration phase 5: root container renamed to h-root"); + let _ = std::fs::write(hroot_rename_marker(), b"done\n"); +} + async fn enumerate_agents() -> Vec { let containers = lifecycle::list().await.unwrap_or_default(); containers .into_iter() .filter_map(|c| { - if c == MANAGER_NAME { + if c == MANAGER_CONTAINER { Some(MANAGER_NAME.to_owned()) } else { c.strip_prefix(AGENT_PREFIX).map(str::to_owned)