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
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

View file

@ -175,9 +175,12 @@ pub fn write(topology: &BTreeMap<String, Option<String>>) -> 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<String, Option<String>>) -> std::io::Result<()>
pub fn default_seed(agent_names: &[String]) -> BTreeMap<String, Option<String>> {
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]