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
|
name == MANAGER_NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The nixosConfiguration in the hyperhive flake the agent's `flake.nix`
|
/// The nixosConfiguration in the hyperhive flake the agent's
|
||||||
/// extends. Manager → `manager`; everyone else → `agent-base`.
|
/// wrapper extends. Manager → `manager`; everyone else →
|
||||||
|
/// `agent-base`. Used by the meta-flake generator to know which
|
||||||
|
/// base to extend per agent.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
#[allow(dead_code)] // wired up by the meta module in a follow-up commit
|
||||||
pub fn flake_base(name: &str) -> &'static str {
|
pub fn flake_base(name: &str) -> &'static str {
|
||||||
if is_manager(name) {
|
if is_manager(name) {
|
||||||
"manager"
|
"manager"
|
||||||
|
|
@ -136,13 +139,16 @@ async fn port_collision(self_name: &str) -> Option<String> {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn spawn(
|
pub async fn spawn(
|
||||||
name: &str,
|
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,
|
agent_dir: &Path,
|
||||||
proposed_dir: &Path,
|
proposed_dir: &Path,
|
||||||
applied_dir: &Path,
|
applied_dir: &Path,
|
||||||
claude_dir: &Path,
|
claude_dir: &Path,
|
||||||
notes_dir: &Path,
|
notes_dir: &Path,
|
||||||
dashboard_port: u16,
|
_dashboard_port: u16,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
validate(name)?;
|
validate(name)?;
|
||||||
if let Some(other) = port_collision(name).await {
|
if let Some(other) = port_collision(name).await {
|
||||||
|
|
@ -152,14 +158,7 @@ pub async fn spawn(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setup_proposed(proposed_dir, name).await?;
|
setup_proposed(proposed_dir, name).await?;
|
||||||
setup_applied(
|
setup_applied(applied_dir, Some(proposed_dir), name).await?;
|
||||||
applied_dir,
|
|
||||||
Some(proposed_dir),
|
|
||||||
name,
|
|
||||||
hyperhive_flake,
|
|
||||||
dashboard_port,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
ensure_claude_dir(claude_dir)?;
|
ensure_claude_dir(claude_dir)?;
|
||||||
ensure_state_dir(notes_dir)?;
|
ensure_state_dir(notes_dir)?;
|
||||||
let container = container_name(name);
|
let container = container_name(name);
|
||||||
|
|
@ -223,12 +222,14 @@ pub async fn destroy(name: &str) -> Result<()> {
|
||||||
|
|
||||||
pub async fn rebuild(
|
pub async fn rebuild(
|
||||||
name: &str,
|
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,
|
agent_dir: &Path,
|
||||||
applied_dir: &Path,
|
applied_dir: &Path,
|
||||||
claude_dir: &Path,
|
claude_dir: &Path,
|
||||||
notes_dir: &Path,
|
notes_dir: &Path,
|
||||||
dashboard_port: u16,
|
_dashboard_port: u16,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
validate(name)?;
|
validate(name)?;
|
||||||
if let Some(other) = port_collision(name).await {
|
if let Some(other) = port_collision(name).await {
|
||||||
|
|
@ -237,7 +238,7 @@ pub async fn rebuild(
|
||||||
agent_web_port(name)
|
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_claude_dir(claude_dir)?;
|
||||||
ensure_state_dir(notes_dir)?;
|
ensure_state_dir(notes_dir)?;
|
||||||
let container = container_name(name);
|
let container = container_name(name);
|
||||||
|
|
@ -272,10 +273,17 @@ pub async fn list() -> Result<Vec<String>> {
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the manager-editable proposed repo. Contains only `agent.nix`
|
/// Initialize the manager-editable proposed repo. Seeds two tracked
|
||||||
/// (the file the manager edits). Touched by hive-c0re only on first spawn —
|
/// files: `agent.nix` (the module the manager edits) and `flake.nix`
|
||||||
/// never again — so the manager can't be surprised by hive-c0re commits or
|
/// (the boilerplate that lets the meta flake import this repo as an
|
||||||
/// working-tree resets.
|
/// 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<()> {
|
pub async fn setup_proposed(proposed_dir: &Path, name: &str) -> Result<()> {
|
||||||
if proposed_dir.join(".git").exists() {
|
if proposed_dir.join(".git").exists() {
|
||||||
return Ok(());
|
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))
|
std::fs::write(&agent_path, initial_agent_nix(name))
|
||||||
.with_context(|| format!("write {}", agent_path.display()))?;
|
.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, &["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?;
|
git_commit(proposed_dir, "hive-c0re init").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set up the applied repo. Two responsibilities:
|
/// Set up the applied repo. First-spawn only: init the repo, pull
|
||||||
/// - First-spawn only: init the repo, pull proposed's initial commit
|
/// proposed's initial commit in via `git fetch`, tag it `deployed/0`.
|
||||||
/// in via `git fetch`, tag it `deployed/0`. This is the *only* time
|
/// This is the *only* time hive-c0re reads from `proposed` for an
|
||||||
/// hive-c0re reads from `proposed` for an agent — subsequent
|
/// agent — subsequent proposals are fetched at `request_apply_commit`
|
||||||
/// proposals are fetched at `request_apply_commit` time and tagged
|
/// time and tagged `proposal/<id>` (see `actions::approve` for the
|
||||||
/// `proposal/<id>` (see `actions::approve` for the tag state
|
/// tag state machine).
|
||||||
/// machine).
|
|
||||||
/// - Every call: regenerate the untracked `flake.nix` so flake-url /
|
|
||||||
/// dashboard-port changes pick up on rebuild without churning the
|
|
||||||
/// git log.
|
|
||||||
///
|
///
|
||||||
/// `proposed_dir` is `None` on rebuild paths that just want the flake
|
/// `proposed_dir` is `None` on rebuild paths where the repo already
|
||||||
/// refreshed.
|
/// 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(
|
pub async fn setup_applied(
|
||||||
applied_dir: &Path,
|
applied_dir: &Path,
|
||||||
proposed_dir: Option<&Path>,
|
proposed_dir: Option<&Path>,
|
||||||
name: &str,
|
name: &str,
|
||||||
hyperhive_flake: &str,
|
|
||||||
dashboard_port: u16,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
std::fs::create_dir_all(applied_dir)
|
std::fs::create_dir_all(applied_dir)
|
||||||
.with_context(|| format!("create {}", applied_dir.display()))?;
|
.with_context(|| format!("create {}", applied_dir.display()))?;
|
||||||
|
|
||||||
// 1. First-spawn git init from proposed (or pre-overhaul detection).
|
|
||||||
if !applied_dir.join(".git").exists() {
|
if !applied_dir.join(".git").exists() {
|
||||||
let Some(proposed) = proposed_dir else {
|
let Some(proposed) = proposed_dir else {
|
||||||
bail!(
|
bail!(
|
||||||
|
|
@ -339,60 +348,18 @@ pub async fn setup_applied(
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
// Pre-overhaul applied repo — agent.nix is tracked directly,
|
// Pre-overhaul applied repo — no deployed/* tag scheme,
|
||||||
// commits authored by hive-c0re, no deployed/* tag scheme.
|
// flake.nix may be untracked, agent.nix possibly authored by
|
||||||
// No in-place migration; fail loudly so the operator purges.
|
// hive-c0re directly. The startup auto-migration fixes this
|
||||||
|
// in place; if it didn't run (or got skipped), surface a
|
||||||
|
// clear error.
|
||||||
bail!(
|
bail!(
|
||||||
"applied repo at {} predates the tag-driven config flow. \
|
"applied repo at {} predates the meta-flake layout. \
|
||||||
Run `hive-c0re destroy --purge {name}` and re-spawn.",
|
Restart hive-c0re to let the auto-migration run, or \
|
||||||
|
destroy --purge {name} and re-spawn.",
|
||||||
applied_dir.display()
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,10 +390,18 @@ fn ensure_state_dir(notes_dir: &Path) -> Result<()> {
|
||||||
|
|
||||||
fn initial_agent_nix(name: &str) -> String {
|
fn initial_agent_nix(name: &str) -> String {
|
||||||
format!(
|
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<()> {
|
async fn git_commit(dir: &Path, message: &str) -> Result<()> {
|
||||||
git(
|
git(
|
||||||
dir,
|
dir,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue