lifecycle: module-only agent flake.nix, tracked in proposed
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.
This commit is contained in:
parent
a1cfb60fd0
commit
5b5a93e0c6
1 changed files with 61 additions and 86 deletions
|
|
@ -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<String> {
|
|||
#[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<Vec<String>> {
|
|||
.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/<id>` (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/<id>` (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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue