topology: meta-repo agent hierarchy + ContainerView.parent (#361)

This commit is contained in:
damocles 2026-05-24 04:47:55 +02:00
commit 0b03d5bcfb
6 changed files with 403 additions and 3 deletions

View file

@ -85,6 +85,16 @@ pub async fn sync_agents(
std::fs::write(&flake_path, &new_flake)
.with_context(|| format!("write {}", flake_path.display()))?;
// Reconcile topology.json against the live agent set — adds
// entries for newly-spawned agents (default: manager as parent,
// manager itself as root) and drops removed agents. Operator
// overrides via the write API (#361 follow-up) are preserved
// because reconcile only fills in missing entries. Idempotent;
// when nothing changed the file isn't touched.
let agent_names: Vec<String> = agents.iter().map(|a| a.name.clone()).collect();
let topology_changed = crate::topology::reconcile(&agent_names)
.with_context(|| format!("reconcile {}", crate::topology::topology_path().display()))?;
if initial {
git(&dir, &["init", "--initial-branch=main"]).await?;
}
@ -96,12 +106,20 @@ pub async fn sync_agents(
// contain '/flake.nix'". Lock then commit once with both
// flake.nix and flake.lock — single commit per change.
git(&dir, &["add", "flake.nix"]).await?;
// Stage topology.json on every sync (regenerated by reconcile
// above when the agent set changed). git add is a no-op when the
// file content is unchanged.
if crate::topology::topology_path().exists() {
git(&dir, &["add", "topology.json"]).await?;
}
nix(&dir, &["flake", "lock"]).await?;
if std::path::Path::new(&dir).join("flake.lock").exists() {
git(&dir, &["add", "flake.lock"]).await?;
}
let msg = if initial {
format!("seed meta from {} agent(s)", agents.len())
} else if topology_changed {
"regenerate meta flake + topology".to_owned()
} else {
"regenerate meta flake".to_owned()
};
@ -348,7 +366,7 @@ where
let pronouns_escaped = operator_pronouns.replace('\\', "\\\\").replace('"', "\\\"");
let _ = writeln!(
out,
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port }}:"
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port, parent ? null }}:"
);
out.push_str(
r#" let
@ -357,6 +375,7 @@ where
else hyperhive.nixosConfigurations.agent-base;
input = inputs."agent-${name}";
service = if isManager then "hive-m1nd" else "hive-ag3nt";
parentEnv = if parent == null then {} else { HIVE_PARENT = parent; };
in
base.extendModules {
modules = [
@ -372,7 +391,7 @@ where
HIVE_LABEL = name;
HYPERHIVE_STATE_DIR = "/agents/${name}/state";
};
systemd.services.${service}.environment = {
systemd.services.${service}.environment = parentEnv // {
HIVE_PORT = toString port;
HIVE_LABEL = name;
HIVE_DASHBOARD_PORT = toString dashboardPort;
@ -406,14 +425,25 @@ where
nixosConfigurations = {
"#,
);
// Pull the topology map once and look up each agent's parent. An
// empty / absent topology.json yields `parent = null` for everyone
// — equivalent to the pre-#361 status quo (every container at root).
// `meta::sync_agents` seeds the file on first run with manager as
// root + everyone else under manager.
let topology = crate::topology::read();
for spec in agents {
let parent_attr = topology
.get(&spec.name)
.and_then(|p| p.as_ref())
.map_or_else(|| "null".to_owned(), |p| format!("\"{p}\""));
let _ = writeln!(
out,
" {} = mkAgent {{ name = \"{}\"; isManager = {}; port = {}; }};",
" {} = mkAgent {{ name = \"{}\"; isManager = {}; port = {}; parent = {}; }};",
spec.name,
spec.name,
if spec.is_manager { "true" } else { "false" },
spec.port,
parent_attr,
);
}
out.push_str(" };\n };\n}\n");