hive-c0re: default new agents to root instead of under the bootstrap container

This commit is contained in:
damocles 2026-06-21 23:28:06 +02:00 committed by mara
commit 513c828ab2
2 changed files with 32 additions and 39 deletions

View file

@ -67,9 +67,11 @@ where system-level facts live.
empty map → every agent treated as root (safe degradation for empty map → every agent treated as root (safe degradation for
fresh installs that haven't run `meta::sync_agents` yet). fresh installs that haven't run `meta::sync_agents` yet).
2. **Reconcile**: `meta::sync_agents` calls `topology::reconcile` 2. **Reconcile**: `meta::sync_agents` calls `topology::reconcile`
alongside its `flake.nix` regeneration. New agents land at their alongside its `flake.nix` regeneration. New agents default to root
default position (manager as parent, manager itself as root); (null parent) — an agent-requested sub-agent already carries an
removed agents drop. Existing entries are preserved as-is so 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 operator overrides stick across regenerations. Pending-init agents
(an operator-approved proposed config repo but no container yet — (an operator-approved proposed config repo but no container yet —
`Coordinator::pending_init_names`) are kept too, so the `Coordinator::pending_init_names`) are kept too, so the

View file

@ -175,9 +175,12 @@ pub fn write(topology: &BTreeMap<String, Option<String>>) -> std::io::Result<()>
std::fs::write(&path, format!("{text}\n")) std::fs::write(&path, format!("{text}\n"))
} }
/// Compute the default topology for a fresh install: every non-manager /// Compute the default topology for a fresh install: every agent is a
/// agent has the manager as parent; manager itself is root. Used by /// root (parent = null). There is no structural "manager" — agents
/// `meta::sync_agents` on first call to seed `topology.json`. /// 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` /// As soon as an explicit write lands (dashboard / `RequestSetParent`
/// API), this seeding stops touching pre-existing entries — /// API), this seeding stops touching pre-existing entries —
@ -192,14 +195,7 @@ pub fn write(topology: &BTreeMap<String, Option<String>>) -> std::io::Result<()>
pub fn default_seed(agent_names: &[String]) -> BTreeMap<String, Option<String>> { pub fn default_seed(agent_names: &[String]) -> BTreeMap<String, Option<String>> {
let mut out = BTreeMap::new(); let mut out = BTreeMap::new();
for name in agent_names { for name in agent_names {
if name == crate::lifecycle::MANAGER_NAME { out.insert(name.clone(), None);
out.insert(name.clone(), None);
} else {
out.insert(
name.clone(),
Some(crate::lifecycle::MANAGER_NAME.to_owned()),
);
}
} }
out out
} }
@ -355,12 +351,14 @@ pub fn apply_reconcile(
let mut changed = false; let mut changed = false;
for name in agent_names { for name in agent_names {
if !next.contains_key(name) { if !next.contains_key(name) {
let parent = if name == crate::lifecycle::MANAGER_NAME { // A new agent with no declared parent defaults to root
None // (parent = null). Agent-requested sub-agents always carry an
} else { // explicit requester-as-parent edge (written at init_config
Some(crate::lifecycle::MANAGER_NAME.to_owned()) // approval), so they never hit this default — only
}; // user/operator-initiated spawns do, and those are roots. No
next.insert(name.clone(), parent); // agent is structurally privileged here: "root-ness" is just
// a null parent.
next.insert(name.clone(), None);
changed = true; changed = true;
} }
} }
@ -491,26 +489,19 @@ mod tests {
use super::*; use super::*;
#[test] #[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![ let agents = vec![
"alice".to_owned(), "alice".to_owned(),
crate::lifecycle::MANAGER_NAME.to_owned(), crate::lifecycle::MANAGER_NAME.to_owned(),
"bob".to_owned(), "bob".to_owned(),
]; ];
let seed = default_seed(&agents); let seed = default_seed(&agents);
assert_eq!( assert_eq!(seed.get(crate::lifecycle::MANAGER_NAME), Some(&None));
seed.get(crate::lifecycle::MANAGER_NAME), assert_eq!(seed.get("alice"), Some(&None));
Some(&None), assert_eq!(seed.get("bob"), 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()))
);
} }
#[test] #[test]
@ -665,17 +656,17 @@ mod tests {
} }
#[test] #[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![ let live = vec![
crate::lifecycle::MANAGER_NAME.to_owned(), crate::lifecycle::MANAGER_NAME.to_owned(),
"newbie".to_owned(), "newbie".to_owned(),
]; ];
let (next, changed) = apply_reconcile(&BTreeMap::new(), &live, &[]); let (next, changed) = apply_reconcile(&BTreeMap::new(), &live, &[]);
assert!(changed); assert!(changed);
assert_eq!( assert_eq!(next.get("newbie"), Some(&None));
next.get("newbie"), assert_eq!(next.get(crate::lifecycle::MANAGER_NAME), Some(&None));
Some(&Some(crate::lifecycle::MANAGER_NAME.to_owned()))
);
} }
#[test] #[test]