diff --git a/hive-c0re/src/topology.rs b/hive-c0re/src/topology.rs index 62affecc..49a2ddd9 100644 --- a/hive-c0re/src/topology.rs +++ b/hive-c0re/src/topology.rs @@ -1,31 +1,13 @@ //! Agent topology storage — single source of truth for parent/child -//! relations in the hive. Lives in the hive-c0re-owned meta repo at -//! `/var/lib/hyperhive/meta/topology.json`, alongside `flake.nix`, so -//! topology changes thread through the same git commit log as deploys. +//! relations in the hive. Persisted as a flat JSON map of `name → +//! parent name | null` at `/var/lib/hyperhive/meta/topology.json`, +//! alongside the meta `flake.nix`, so topology changes thread through +//! the same git commit log as deploys. //! -//! Why meta, not per-agent: an agent shouldn't be able to claim a -//! parent without that parent's consent, and an operator-driven -//! re-parenting shouldn't require touching the moved agent's own -//! config. Topology IS a system-level concern; meta is where -//! system-level facts live. -//! -//! Format — flat JSON map keyed by agent name, values are the parent -//! agent's name or `null` for root: -//! -//! ```json -//! { -//! "manager": null, -//! "alice": "manager", -//! "bob": "alice" -//! } -//! ``` -//! -//! Agents present in `nixos-container list` but absent from the file -//! default to root-level (`parent = None`). This file is operator/ -//! manager-managed via approval-gated writes (write API lands in a -//! follow-up PR on the #361 milestone); for the bootstrap commit -//! `meta::sync_agents` seeds it with the existing implicit topology -//! (manager as root, all current sub-agents as direct children). +//! Format, rationale, read/reconcile/inject/surface flow, and target +//! enforcement semantics: `docs/agent-hierarchy.md::Current state`. +//! `` sentinel resolution (delivered by [`resolve_recipient`]): +//! `docs/conventions.md::Recipient sentinels`. use std::collections::BTreeMap; use std::path::PathBuf; @@ -64,25 +46,13 @@ pub fn parent_of(name: &str) -> Option { /// Resolve a magic recipient sentinel (currently just /// [`hive_sh4re::PARENT_RECIPIENT`]) to a real broker recipient at -/// send time. Lets agents address structural roles -/// (`send(to: "", ...)`) without learning their parent's -/// label — runtime reparenting (#486) propagates for free. -/// -/// Resolution rules: -/// - `` → `topology.json`'s parent for `sender` if some, -/// else [`hive_sh4re::OPERATOR_RECIPIENT`] (the "no parent → tell -/// mara" fallback from #692 / #8592). -/// - Anything else: returned unchanged. -/// -/// Returns an owned `String` for the rewritten recipient so callers -/// can plug it straight into [`crate::broker::Broker::send`] without +/// send time. Returns an owned `String` so callers can plug it +/// straight into [`crate::broker::Broker::send`] without /// borrow-juggling around the temporary lookup. /// -/// Fast path: ordinary recipient names (the overwhelming majority of -/// sends) short-circuit before touching the disk — only `` -/// triggers the `read()` of `topology.json`. Sentinel-free traffic -/// pays a single string compare; the disk read is amortised across -/// every `` send (per argus on #703#issuecomment-8750). +/// Rules + rationale: `docs/conventions.md::Recipient sentinels`. +/// Fast path: ordinary recipient names short-circuit before any +/// disk read — only `` triggers `read()` on `topology.json`. #[must_use] pub fn resolve_recipient(sender: &str, to: &str) -> String { // Early exit: only sentinel recipients need topology lookup. This @@ -156,15 +126,15 @@ pub fn write(topology: &BTreeMap>) -> std::io::Result<()> /// agent has the manager as parent; manager itself is root. Used by /// `meta::sync_agents` on first call to seed `topology.json`. /// -/// As soon as an explicit write lands (#361 follow-up: dashboard / -/// `RequestSetParent` API), this seeding stops touching pre-existing -/// entries — `sync_agents` only adds rows for newly-spawned agents -/// against whatever the operator has configured. +/// As soon as an explicit write lands (dashboard / `RequestSetParent` +/// API), this seeding stops touching pre-existing entries — +/// `sync_agents` only adds rows for newly-spawned agents against +/// whatever the operator has configured. #[must_use] #[allow( dead_code, - reason = "kept for the dashboard / RequestSetParent write API landing in \ - #361 follow-ups; `sync_agents` does its own seeding today" + reason = "kept for the dashboard / RequestSetParent write API; \ + `sync_agents` does its own seeding today" )] pub fn default_seed(agent_names: &[String]) -> BTreeMap> { let mut out = BTreeMap::new(); @@ -187,13 +157,11 @@ pub fn default_seed(agent_names: &[String]) -> BTreeMap> /// either the post-move map (caller writes it back) or a /// user-readable error string. /// -/// Pre-#743 this also refused to reparent the manager -/// ("cannot reparent the manager — it is structurally root") — -/// argus-paranoia from #361 that we dropped per mara's -/// `#9512` / `#9557`: the manager's special powers come from its -/// privileged MCP socket, not its tree position. The cycle walk -/// below covers "moving X under its own descendant" for the -/// manager as much as any other agent. +/// The manager is reparentable like any other agent — its special +/// powers come from the privileged MCP socket, not its tree +/// position. The cycle walk below covers "moving X under its own +/// descendant" for the manager as much as any other agent. +/// `docs/agent-hierarchy.md::Current state` has the rationale. pub fn apply_set_parent( topo: &BTreeMap>, child: &str, @@ -230,16 +198,15 @@ pub fn apply_set_parent( Ok(next) } -/// Operator-driven parent move (#486 / #487). Set `child`'s parent -/// to `new_parent` (or `None` to promote to root). See -/// [`apply_set_parent`] for the validation rules. The operator-set -/// parent sticks across `reconcile()` calls (which preserves -/// existing entries). +/// Operator-driven parent move. Set `child`'s parent to `new_parent` +/// (or `None` to promote to root). See [`apply_set_parent`] for the +/// validation rules. The operator-set parent sticks across +/// `reconcile()` calls (which preserves existing entries). /// /// No bind-mount / container churn today — the hierarchy is -/// currently logical-only (see #486 comment 5042). Once -/// sub-manager bind mounts land alongside #361, the caller adds -/// an umount-old / mount-new / restart-cascade step on top. +/// currently logical-only. Once sub-manager bind mounts land, the +/// caller adds an umount-old / mount-new / restart-cascade step on +/// top. pub fn set_parent(child: &str, new_parent: Option<&str>) -> Result<(), String> { let current = read(); // Idempotent no-op fast path: skip the disk write when nothing @@ -348,18 +315,18 @@ mod tests { #[test] fn apply_set_parent_allows_manager_move() { - // Post-#743: the manager is reparentable like any other agent - // (its privileges live on the MCP socket, not its tree - // position). Build a topo with an unrelated root-level agent - // `peer` so moving the manager under it doesn't trip the - // cycle walk (every non-manager agent in topo_three_level - // descends from the manager, so that fixture can't exercise - // a legal manager move). + // The manager is reparentable like any other agent (its + // privileges live on the MCP socket, not its tree position). + // Build a topo with an unrelated root-level agent `peer` so + // moving the manager under it doesn't trip the cycle walk + // (every non-manager agent in topo_three_level descends from + // the manager, so that fixture can't exercise a legal + // manager move). let mut topo = BTreeMap::new(); topo.insert(crate::lifecycle::MANAGER_NAME.to_owned(), None); topo.insert("peer".to_owned(), None); let next = apply_set_parent(&topo, crate::lifecycle::MANAGER_NAME, Some("peer")) - .expect("manager move should succeed post-#743"); + .expect("manager move should succeed"); assert_eq!( next.get(crate::lifecycle::MANAGER_NAME), Some(&Some("peer".to_owned())) @@ -458,8 +425,8 @@ mod tests { fn resolve_recipient_falls_back_to_operator_for_root_agent() { let topo = topo_three_level(); // Manager is structurally root (parent = None) → `` - // resolves to the operator (matching the "no parent → tell mara" - // rule from #692#issuecomment-8592). + // resolves to the operator (the "no parent → tell mara" + // fallback documented in conventions.md). assert_eq!( resolve_recipient_in( &topo,