diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 65595d78..0a30a9df 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -1156,7 +1156,7 @@ fn set_nspawn_flags( agent_name, crate::topology::ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS, ) { - let top_level = crate::topology::children_of(MANAGER_NAME); + let top_level = crate::topology::top_level_agents(); for tl in &top_level { if !direct_children.contains(tl) { bind_child_agent_dirs(tl, &mut binds); diff --git a/hive-c0re/src/topology.rs b/hive-c0re/src/topology.rs index 662c7586..01378307 100644 --- a/hive-c0re/src/topology.rs +++ b/hive-c0re/src/topology.rs @@ -74,6 +74,34 @@ pub fn children_of_in( .collect() } +/// Return the agents that are direct children of the topology root +/// (the unique agent whose own parent is `None`). These are the +/// "top-level" agents — one layer below the auto-managed manager. +/// +/// Callers should use this instead of `children_of(MANAGER_NAME)` so +/// the manager's logical name is not hardcoded outside lifecycle.rs. +/// If the topology has no root agent (e.g. empty file on cold boot), +/// returns an empty vec. +#[must_use] +pub fn top_level_agents() -> Vec { + top_level_agents_in(&read()) +} + +/// Pure form of [`top_level_agents`] for unit tests. +#[must_use] +pub fn top_level_agents_in(topo: &BTreeMap>) -> Vec { + // Find the unique root (parent = None). If none or multiple exist, + // fall back to empty — the topology is malformed or uninitialised. + let roots: Vec<&str> = topo + .iter() + .filter_map(|(name, parent)| if parent.is_none() { Some(name.as_str()) } else { None }) + .collect(); + match roots.as_slice() { + [root] => children_of_in(topo, root), + _ => vec![], + } +} + /// Resolve a magic recipient sentinel (currently just /// [`hive_sh4re::PARENT_RECIPIENT`]) to a real broker recipient at /// send time. Returns an owned `String` so callers can plug it @@ -623,6 +651,20 @@ mod tests { assert!(children_of_in(&topo, "nobody").is_empty()); } + #[test] + fn top_level_agents_in_returns_children_of_root() { + let topo = topo_three_level(); + // root's children = [alice]; bob+carol are under alice. + let top = top_level_agents_in(&topo); + assert_eq!(top, vec!["alice"]); + } + + #[test] + fn top_level_agents_in_empty_topo_returns_empty() { + let topo = BTreeMap::new(); + assert!(top_level_agents_in(&topo).is_empty()); + } + // ----------------------------------------------------------------------- // Roles tests (no disk I/O — use the pure `has_role_in` / in-memory maps) // -----------------------------------------------------------------------