From 5b5a93e0c6b3eeb275a7a2f1b8e6ce0d5027a353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sat, 16 May 2026 00:10:06 +0200 Subject: [PATCH] lifecycle: module-only agent flake.nix, tracked in proposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup_proposed now seeds both agent.nix (a regular NixOS module function) and flake.nix (boilerplate exporting nixosModules.default = import ./agent.nix) into the manager-editable proposed repo, committed together. setup_applied's hyperhive_flake + dashboard port wrapper generation is deleted entirely — the meta flake at /var/lib/hyperhive/meta/ now owns the wrapper module. setup_ applied just fetches proposed's main on first spawn and tags deployed/0; subsequent rebuilds touch nothing in applied that the manager didn't author. spawn + rebuild keep their old param list with the now-unused hyperhive_flake + dashboard_port underscored — call sites get cleaned up after the meta module lands and consumes them. --- hive-c0re/src/lifecycle.rs | 147 +++++++++++++++---------------------- 1 file changed, 61 insertions(+), 86 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 9a1fed3..d71ea23 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -80,9 +80,12 @@ pub fn is_manager(name: &str) -> bool { name == MANAGER_NAME } -/// The nixosConfiguration in the hyperhive flake the agent's `flake.nix` -/// extends. Manager → `manager`; everyone else → `agent-base`. +/// The nixosConfiguration in the hyperhive flake the agent's +/// wrapper extends. Manager → `manager`; everyone else → +/// `agent-base`. Used by the meta-flake generator to know which +/// base to extend per agent. #[must_use] +#[allow(dead_code)] // wired up by the meta module in a follow-up commit pub fn flake_base(name: &str) -> &'static str { if is_manager(name) { "manager" @@ -136,13 +139,16 @@ async fn port_collision(self_name: &str) -> Option { #[allow(clippy::too_many_arguments)] pub async fn spawn( name: &str, - hyperhive_flake: &str, + // hyperhive_flake + dashboard_port are unused now that the meta + // flake owns the wrapper; left here as the caller surface settles + // — meta-module landing will remove them in a follow-up. + _hyperhive_flake: &str, agent_dir: &Path, proposed_dir: &Path, applied_dir: &Path, claude_dir: &Path, notes_dir: &Path, - dashboard_port: u16, + _dashboard_port: u16, ) -> Result<()> { validate(name)?; if let Some(other) = port_collision(name).await { @@ -152,14 +158,7 @@ pub async fn spawn( ); } setup_proposed(proposed_dir, name).await?; - setup_applied( - applied_dir, - Some(proposed_dir), - name, - hyperhive_flake, - dashboard_port, - ) - .await?; + setup_applied(applied_dir, Some(proposed_dir), name).await?; ensure_claude_dir(claude_dir)?; ensure_state_dir(notes_dir)?; let container = container_name(name); @@ -223,12 +222,14 @@ pub async fn destroy(name: &str) -> Result<()> { pub async fn rebuild( name: &str, - hyperhive_flake: &str, + // hyperhive_flake + dashboard_port unused after the meta-flake + // overhaul; kept on the signature until callers are reworked. + _hyperhive_flake: &str, agent_dir: &Path, applied_dir: &Path, claude_dir: &Path, notes_dir: &Path, - dashboard_port: u16, + _dashboard_port: u16, ) -> Result<()> { validate(name)?; if let Some(other) = port_collision(name).await { @@ -237,7 +238,7 @@ pub async fn rebuild( agent_web_port(name) ); } - setup_applied(applied_dir, None, name, hyperhive_flake, dashboard_port).await?; + setup_applied(applied_dir, None, name).await?; ensure_claude_dir(claude_dir)?; ensure_state_dir(notes_dir)?; let container = container_name(name); @@ -272,10 +273,17 @@ pub async fn list() -> Result> { .collect()) } -/// Initialize the manager-editable proposed repo. Contains only `agent.nix` -/// (the file the manager edits). Touched by hive-c0re only on first spawn — -/// never again — so the manager can't be surprised by hive-c0re commits or -/// working-tree resets. +/// Initialize the manager-editable proposed repo. Seeds two tracked +/// files: `agent.nix` (the module the manager edits) and `flake.nix` +/// (the boilerplate that lets the meta flake import this repo as an +/// input — meta locks at a specific sha and reads +/// `nixosModules.default`, so `flake.nix` must be in the commit). The +/// manager shouldn't edit `flake.nix` (the prompt says so) but it's +/// visible so they can introspect. +/// +/// Touched by hive-c0re only on first spawn — never again — so the +/// manager can't be surprised by hive-c0re commits or working-tree +/// resets. pub async fn setup_proposed(proposed_dir: &Path, name: &str) -> Result<()> { if proposed_dir.join(".git").exists() { return Ok(()); @@ -287,36 +295,37 @@ pub async fn setup_proposed(proposed_dir: &Path, name: &str) -> Result<()> { std::fs::write(&agent_path, initial_agent_nix(name)) .with_context(|| format!("write {}", agent_path.display()))?; } + let flake_path = proposed_dir.join("flake.nix"); + if !flake_path.exists() { + std::fs::write(&flake_path, initial_flake_nix()) + .with_context(|| format!("write {}", flake_path.display()))?; + } git(proposed_dir, &["init", "--initial-branch=main"]).await?; - git(proposed_dir, &["add", "agent.nix"]).await?; + git(proposed_dir, &["add", "agent.nix", "flake.nix"]).await?; git_commit(proposed_dir, "hive-c0re init").await?; Ok(()) } -/// Set up the applied repo. Two responsibilities: -/// - First-spawn only: init the repo, pull proposed's initial commit -/// in via `git fetch`, tag it `deployed/0`. This is the *only* time -/// hive-c0re reads from `proposed` for an agent — subsequent -/// proposals are fetched at `request_apply_commit` time and tagged -/// `proposal/` (see `actions::approve` for the tag state -/// machine). -/// - Every call: regenerate the untracked `flake.nix` so flake-url / -/// dashboard-port changes pick up on rebuild without churning the -/// git log. +/// Set up the applied repo. First-spawn only: init the repo, pull +/// proposed's initial commit in via `git fetch`, tag it `deployed/0`. +/// This is the *only* time hive-c0re reads from `proposed` for an +/// agent — subsequent proposals are fetched at `request_apply_commit` +/// time and tagged `proposal/` (see `actions::approve` for the +/// tag state machine). /// -/// `proposed_dir` is `None` on rebuild paths that just want the flake -/// refreshed. +/// `proposed_dir` is `None` on rebuild paths where the repo already +/// exists — we just verify it's the right shape and bail otherwise. +/// Unlike the pre-overhaul code path, `flake.nix` is no longer +/// regenerated at the host level: it's tracked in proposed (seeded by +/// `setup_proposed`) and rides along on every fetch. pub async fn setup_applied( applied_dir: &Path, proposed_dir: Option<&Path>, name: &str, - hyperhive_flake: &str, - dashboard_port: u16, ) -> Result<()> { std::fs::create_dir_all(applied_dir) .with_context(|| format!("create {}", applied_dir.display()))?; - // 1. First-spawn git init from proposed (or pre-overhaul detection). if !applied_dir.join(".git").exists() { let Some(proposed) = proposed_dir else { bail!( @@ -339,60 +348,18 @@ pub async fn setup_applied( .await .is_err() { - // Pre-overhaul applied repo — agent.nix is tracked directly, - // commits authored by hive-c0re, no deployed/* tag scheme. - // No in-place migration; fail loudly so the operator purges. + // Pre-overhaul applied repo — no deployed/* tag scheme, + // flake.nix may be untracked, agent.nix possibly authored by + // hive-c0re directly. The startup auto-migration fixes this + // in place; if it didn't run (or got skipped), surface a + // clear error. bail!( - "applied repo at {} predates the tag-driven config flow. \ - Run `hive-c0re destroy --purge {name}` and re-spawn.", + "applied repo at {} predates the meta-flake layout. \ + Restart hive-c0re to let the auto-migration run, or \ + destroy --purge {name} and re-spawn.", applied_dir.display() ); } - - // 2. (Re)write the untracked wrapper flake. Tracked files in the - // working tree (agent.nix and anything the manager committed) are - // untouched. - let port = agent_web_port(name); - let base = flake_base(name); - let service = if is_manager(name) { - "hive-m1nd" - } else { - "hive-ag3nt" - }; - let description = if is_manager(name) { - format!("hyperhive manager {name}") - } else { - format!("hyperhive sub-agent {name}") - }; - let flake_body = format!( - r#"{{ - description = "{description}"; - inputs.hyperhive.url = "{hyperhive_flake}"; - outputs = - {{ hyperhive, ... }}: - {{ - nixosConfigurations.default = hyperhive.nixosConfigurations.{base}.extendModules {{ - modules = [ - ./agent.nix - {{ - programs.git.config.user = {{ - name = "{name}"; - email = "{name}@hyperhive"; - }}; - systemd.services.{service}.environment = {{ - HIVE_PORT = "{port}"; - HIVE_LABEL = "{name}"; - HIVE_DASHBOARD_PORT = "{dashboard_port}"; - }}; - }} - ]; - }}; - }}; -}} -"#, - ); - std::fs::write(applied_dir.join("flake.nix"), flake_body) - .with_context(|| format!("write {}/flake.nix", applied_dir.display()))?; Ok(()) } @@ -423,10 +390,18 @@ fn ensure_state_dir(notes_dir: &Path) -> Result<()> { fn initial_agent_nix(name: &str) -> String { format!( - "{{ ... }}:\n{{\n # Per-agent overrides for {name}. The manager edits this\n # file (and commits) to customise the agent's NixOS config.\n}}\n", + "{{ config, pkgs, lib, ... }}:\n{{\n # Per-agent overrides for {name}. This is a regular NixOS module\n # — add packages, services, modules, imports as needed.\n #\n # imports = [ ./extra-module.nix ];\n # environment.systemPackages = with pkgs; [ ];\n}}\n", ) } +/// Module-only flake exposed by every agent's repo. Consumed by the +/// hive-c0re-owned meta flake at `/var/lib/hyperhive/meta/` as a flake +/// input. Identity injection (HIVE_PORT / HIVE_LABEL / dashboard port / +/// git committer) lives in the meta flake's wrapper, not here. +fn initial_flake_nix() -> &'static str { + "{\n description = \"hyperhive agent\";\n inputs = { };\n outputs = { self }: {\n nixosModules.default = import ./agent.nix;\n };\n}\n" +} + async fn git_commit(dir: &Path, message: &str) -> Result<()> { git( dir,