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