From 513c828ab2d590a543b27fb636bca7bc8a5133a7 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 21 Jun 2026 23:28:06 +0200 Subject: [PATCH] hive-c0re: default new agents to root instead of under the bootstrap container --- docs/agent-hierarchy.md | 8 +++-- hive-c0re/src/topology.rs | 63 +++++++++++++++++---------------------- 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/docs/agent-hierarchy.md b/docs/agent-hierarchy.md index f6e6be8d..55bc3c46 100644 --- a/docs/agent-hierarchy.md +++ b/docs/agent-hierarchy.md @@ -67,9 +67,11 @@ where system-level facts live. empty map → every agent treated as root (safe degradation for fresh installs that haven't run `meta::sync_agents` yet). 2. **Reconcile**: `meta::sync_agents` calls `topology::reconcile` - alongside its `flake.nix` regeneration. New agents land at their - default position (manager as parent, manager itself as root); - removed agents drop. Existing entries are preserved as-is so + alongside its `flake.nix` regeneration. New agents default to root + (null parent) — an agent-requested sub-agent already carries an + explicit requester-as-parent edge from its `init_config` approval, so + only user/operator-initiated spawns hit this default, and those are + roots; removed agents drop. Existing entries are preserved as-is so operator overrides stick across regenerations. Pending-init agents (an operator-approved proposed config repo but no container yet — `Coordinator::pending_init_names`) are kept too, so the diff --git a/hive-c0re/src/topology.rs b/hive-c0re/src/topology.rs index 3656c2a0..e2e94476 100644 --- a/hive-c0re/src/topology.rs +++ b/hive-c0re/src/topology.rs @@ -175,9 +175,12 @@ pub fn write(topology: &BTreeMap>) -> std::io::Result<()> std::fs::write(&path, format!("{text}\n")) } -/// Compute the default topology for a fresh install: every non-manager -/// agent has the manager as parent; manager itself is root. Used by -/// `meta::sync_agents` on first call to seed `topology.json`. +/// Compute the default topology for a fresh install: every agent is a +/// root (parent = null). There is no structural "manager" — agents +/// arrange themselves via explicit parent edges (an agent-requested +/// sub-agent gets a requester-as-parent edge at `init_config`; the +/// operator reparents via the dashboard / `RequestSetParent` API). +/// Used by `meta::sync_agents` on first call to seed `topology.json`. /// /// As soon as an explicit write lands (dashboard / `RequestSetParent` /// API), this seeding stops touching pre-existing entries — @@ -192,14 +195,7 @@ pub fn write(topology: &BTreeMap>) -> std::io::Result<()> pub fn default_seed(agent_names: &[String]) -> BTreeMap> { let mut out = BTreeMap::new(); for name in agent_names { - if name == crate::lifecycle::MANAGER_NAME { - out.insert(name.clone(), None); - } else { - out.insert( - name.clone(), - Some(crate::lifecycle::MANAGER_NAME.to_owned()), - ); - } + out.insert(name.clone(), None); } out } @@ -355,12 +351,14 @@ pub fn apply_reconcile( let mut changed = false; for name in agent_names { if !next.contains_key(name) { - let parent = if name == crate::lifecycle::MANAGER_NAME { - None - } else { - Some(crate::lifecycle::MANAGER_NAME.to_owned()) - }; - next.insert(name.clone(), parent); + // A new agent with no declared parent defaults to root + // (parent = null). Agent-requested sub-agents always carry an + // explicit requester-as-parent edge (written at init_config + // approval), so they never hit this default — only + // user/operator-initiated spawns do, and those are roots. No + // agent is structurally privileged here: "root-ness" is just + // a null parent. + next.insert(name.clone(), None); changed = true; } } @@ -491,26 +489,19 @@ mod tests { use super::*; #[test] - fn default_seed_makes_manager_root_others_children() { + fn default_seed_makes_every_agent_root() { + // No structural manager: every agent defaults to root (null + // parent). Explicit edges (init_config / dashboard) are layered + // on later. let agents = vec![ "alice".to_owned(), crate::lifecycle::MANAGER_NAME.to_owned(), "bob".to_owned(), ]; let seed = default_seed(&agents); - assert_eq!( - seed.get(crate::lifecycle::MANAGER_NAME), - Some(&None), - "manager should be root" - ); - assert_eq!( - seed.get("alice"), - Some(&Some(crate::lifecycle::MANAGER_NAME.to_owned())) - ); - assert_eq!( - seed.get("bob"), - Some(&Some(crate::lifecycle::MANAGER_NAME.to_owned())) - ); + assert_eq!(seed.get(crate::lifecycle::MANAGER_NAME), Some(&None)); + assert_eq!(seed.get("alice"), Some(&None)); + assert_eq!(seed.get("bob"), Some(&None)); } #[test] @@ -665,17 +656,17 @@ mod tests { } #[test] - fn apply_reconcile_adds_missing_live_agent_under_manager() { + fn apply_reconcile_adds_missing_live_agent_as_root() { + // A live agent with no prior topology entry defaults to root + // (null parent) — no structural manager to hang it under. let live = vec![ crate::lifecycle::MANAGER_NAME.to_owned(), "newbie".to_owned(), ]; let (next, changed) = apply_reconcile(&BTreeMap::new(), &live, &[]); assert!(changed); - assert_eq!( - next.get("newbie"), - Some(&Some(crate::lifecycle::MANAGER_NAME.to_owned())) - ); + assert_eq!(next.get("newbie"), Some(&None)); + assert_eq!(next.get(crate::lifecycle::MANAGER_NAME), Some(&None)); } #[test]