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}