diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index b23db43b..a90e7cfa 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -1149,10 +1149,9 @@ fn set_nspawn_flags( } // `can_manage_top_level_agents` role: additionally mount every - // top-level agent (direct child of the manager) as a virtual child. - // Enables recovery — a role holder can update a top-level agent's - // config even when that agent is down. Also grants RO access to - // /applied and /meta. + // parentless agent in the topology as a virtual child. Enables + // recovery — a role holder can update those agents' configs even + // when they are down. Also grants RO access to /applied and /meta. if crate::topology::has_role( agent_name, crate::topology::ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS, diff --git a/hive-c0re/src/topology.rs b/hive-c0re/src/topology.rs index 2010fa5c..d02d30bb 100644 --- a/hive-c0re/src/topology.rs +++ b/hive-c0re/src/topology.rs @@ -74,13 +74,13 @@ pub fn children_of_in( .collect() } -/// Return the direct children of the manager agent — the "top-level" -/// agents that role holders with `can_manage_top_level_agents` are -/// granted access to. +/// Return every agent that has no parent in the topology. These are the +/// "top-level" agents a `can_manage_top_level_agents` role holder is +/// granted access to. No agent name is hardcoded — the set is derived +/// purely from topology structure. /// -/// Callers outside `topology` should use this instead of -/// `children_of(MANAGER_NAME)` so the manager's logical name stays -/// confined to the topology module. +/// In normal operation this is just the manager, but any agent the +/// operator explicitly places outside the hierarchy is also included. #[must_use] pub fn top_level_agents() -> Vec { top_level_agents_in(&read()) @@ -89,7 +89,9 @@ pub fn top_level_agents() -> Vec { /// Pure form of [`top_level_agents`] for unit tests. #[must_use] pub fn top_level_agents_in(topo: &BTreeMap>) -> Vec { - children_of_in(topo, crate::lifecycle::MANAGER_NAME) + topo.iter() + .filter_map(|(name, parent)| if parent.is_none() { Some(name.clone()) } else { None }) + .collect() } /// Resolve a magic recipient sentinel (currently just @@ -309,10 +311,10 @@ pub fn reconcile(agent_names: &[String]) -> std::io::Result { // Roles // --------------------------------------------------------------------------- -/// Agents with this role have the top-level agents (direct children of the -/// manager) added as virtual children for bind-mount and config-change -/// purposes. Enables recovery: if a top-level agent is down, a role holder -/// can still read its state and update its config. +/// Agents with this role have every parentless agent in the topology +/// (see `top_level_agents`) added as virtual children for bind-mount +/// and config-change purposes. Enables recovery: if a top-level agent +/// is down, a role holder can still read its state and update its config. /// /// The manager receives this role by default on first `reconcile_roles` /// call; operators can revoke it with `set_role`. @@ -642,11 +644,21 @@ mod tests { } #[test] - fn top_level_agents_in_returns_children_of_root() { + fn top_level_agents_in_returns_parentless_agents() { let topo = topo_three_level(); - // root's children = [alice]; bob+carol are under alice. + // Only the manager has no parent (alice/bob/carol all have parents). let top = top_level_agents_in(&topo); - assert_eq!(top, vec!["alice"]); + assert_eq!(top, vec![crate::lifecycle::MANAGER_NAME]); + } + + #[test] + fn top_level_agents_in_multi_root_returns_all_parentless() { + let mut topo = topo_three_level(); + // Simulate a second parentless agent alongside the manager. + topo.insert("orphan".to_owned(), None); + let mut top = top_level_agents_in(&topo); + top.sort(); + assert_eq!(top, vec![crate::lifecycle::MANAGER_NAME, "orphan"]); } #[test]