Skip to main content

majordomus_cli/mesh/
mod.rs

1//! The mesh: how one running Majordomus learns that others exist, and proves which one
2//! it is. ADR 0050 is the decision; this module is the whole implementation, and its
3//! boundaries are the point:
4//!
5//! - **Identity is a key, not an address.** A node is an Ed25519 keypair kept per user
6//!   and machine ([`identity`]); the node id is a digest of the public key. Hostnames,
7//!   IPs, ports and PIDs are runtime attributes and never identity.
8//! - **One protocol** ([`protocol`]): a compact, versioned, bounded, signed envelope,
9//!   the same over multicast, broadcast and rendezvous. Parsing never panics; hostile
10//!   input is a counted refusal.
11//! - **Providers observe, the manager decides** ([`provider`], [`manager`]): a
12//!   discovery mechanism hands raw bytes up and answers for its own health. The manager
13//!   owns the one verification path and the one [`registry`]; adding a mechanism is
14//!   implementing the provider contract, not editing consumers.
15//! - **Discovery creates awareness, not authority** ([`trust`]): a valid unknown node
16//!   is observed and trusted for nothing; even a trusted node gains no execution
17//!   rights — there is no remote execution to gain. The default policy is
18//!   `deny_unknown`, and the mesh itself is off until the repository commits a
19//!   declaration ([`config`]) that says otherwise: the "nothing leaves the machine"
20//!   posture holds by default.
21//! - **Every surface is a projection.** CLI, HTTP, OpenAPI, MCP and the Cockpit read
22//!   [`MeshRuntime`] and the registry; none holds peers of its own.
23//!
24//! Nothing here writes to the repository. The identity file lives under the user's
25//! state directory; the registry lives in process memory and dies with it.
26//!
27//! The whole lifecycle, in memory:
28//!
29//! ```
30//! use majordomus_cli::mesh::{identity::NodeIdentity, protocol};
31//!
32//! // a node proves itself: sign, encode, parse, verify — the one path every
33//! // transport's datagrams take
34//! let node = NodeIdentity::ephemeral().unwrap();
35//! let envelope = protocol::advertise(&node, 1, &["127.0.0.1:8741".into()],
36//!     &["http".into()], &[], "docs");
37//! let bytes = protocol::encode(&envelope).unwrap();
38//! let heard = protocol::parse(&bytes).expect("a fresh signed envelope verifies");
39//! assert_eq!(heard.adv.node_id(), Some(node.public.node_id.clone()));
40//!
41//! // and hostile input is a refusal, never a panic
42//! assert!(protocol::parse(b"garbage").is_err());
43//! ```
44
45pub mod address;
46pub mod broadcast;
47pub mod config;
48pub mod cooperation;
49pub mod doctor;
50pub mod firewall;
51pub mod handover;
52pub mod identity;
53pub mod journal;
54pub mod link;
55pub mod manager;
56pub mod multicast;
57pub mod page;
58pub mod protocol;
59pub mod provider;
60pub mod registry;
61pub mod rendezvous;
62pub mod repository;
63pub mod root;
64pub mod state;
65pub mod trust;
66
67#[cfg(test)]
68pub(crate) mod testing;
69
70pub use config::{MeshConfig, KIND};
71pub use doctor::{DoctorCheck, MeshDoctorReport};
72pub use identity::{default_identity_path, InstanceId, NodeId, NodeIdentity, PublicIdentity};
73pub use manager::{MeshRuntime, MeshStatus, Refusals};
74pub use protocol::{Advertisement, Envelope};
75pub use provider::{MeshProvider, Observation, ProviderContext, ProviderStatus};
76pub use registry::{MeshRegistry, MeshSource, NodeRecord, Presence, Sighting, Tallies};
77pub use rendezvous::RegisterAnswer;
78pub use trust::{TrustPolicy, TrustState};
79
80/// Why a mesh operation did not happen. Each variant names the layer that refused, so
81/// a log line places the failure without a backtrace.
82///
83/// ```
84/// use majordomus_cli::mesh::MeshError;
85/// let e = MeshError::Config("an interval of 0 would be a busy loop".into());
86/// assert_eq!(e.to_string(), "config: an interval of 0 would be a busy loop");
87/// ```
88#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
89pub enum MeshError {
90    /// The identity file: unreadable, malformed, or nowhere to keep one.
91    #[error("identity: {0}")]
92    Identity(String),
93    /// The protocol: an envelope that cannot be built or does not fit.
94    #[error("protocol: {0}")]
95    Protocol(String),
96    /// A provider: a socket, a group, an endpoint.
97    #[error("provider: {0}")]
98    Provider(String),
99    /// The declaration: missing, malformed, or a version this executable does not read.
100    #[error("config: {0}")]
101    Config(String),
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn every_error_layer_names_itself() {
110        for (error, prefix) in [
111            (MeshError::Identity("x".into()), "identity:"),
112            (MeshError::Protocol("x".into()), "protocol:"),
113            (MeshError::Provider("x".into()), "provider:"),
114            (MeshError::Config("x".into()), "config:"),
115        ] {
116            assert!(error.to_string().starts_with(prefix));
117        }
118    }
119}