hive-c0re/topology.rs: trim prose to docs pointers (#715 batch 3)

This commit is contained in:
damocles 2026-05-31 17:11:11 +02:00 committed by mara
commit 4f9697220e

View file

@ -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`.
//! `<parent>` 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<String> {
/// 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: "<parent>", ...)`) without learning their parent's
/// label — runtime reparenting (#486) propagates for free.
///
/// Resolution rules:
/// - `<parent>` → `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 `<parent>`
/// triggers the `read()` of `topology.json`. Sentinel-free traffic
/// pays a single string compare; the disk read is amortised across
/// every `<parent>` 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 `<parent>` 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<String, Option<String>>) -> 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<String, Option<String>> {
let mut out = BTreeMap::new();
@ -187,13 +157,11 @@ pub fn default_seed(agent_names: &[String]) -> BTreeMap<String, Option<String>>
/// 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<String, Option<String>>,
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) → `<parent>`
// 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,