hive-c0re/topology.rs: trim prose to docs pointers (#715 batch 3)
This commit is contained in:
parent
fbc41d42a6
commit
4f9697220e
1 changed files with 41 additions and 74 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue