1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//! The sovereign-group join rule — W305 / R742-F1.
//!
//! One sentence of logic, deliberately given a home of its own because it is
//! asked in two crates that cannot see each other:
//!
//! - **camp-side**, `cloud::judge_join`, which reads two `MachineConfig`s and
//! answers "may these two boxes be in one quorum" while planning;
//! - **node-side**, `yubaba`'s `POST /raft/add-learner` gate, which reads its
//! own `--sovereign-group` and asks the joiner for its own, and refuses.
//!
//! `yubaba` deliberately does **not** depend on `cloud` (R374-F3 moved
//! `local-driver` out precisely to avoid that edge), so the rule cannot simply
//! live in one of them. It lives here for the same reason
//! [`PUBLIC_IP_TAINT`](crate::PUBLIC_IP_TAINT) does: this crate is the shared
//! vocabulary both the planner and the daemon already link, and it depends on
//! neither.
//!
//! What is **not** shared is the prose. A camp-side refusal points at
//! `.yah/infra/machines/<name>.toml`; a node-side refusal has no machine name
//! to interpolate and must also name `yubaba serve --sovereign-group`, because
//! editing the TOML alone does not change what the running daemon declares.
//! Two renderings, one predicate — which is the split that keeps them from
//! disagreeing about what counts as a refusal.
/// May a node declaring `joiner` join a cluster whose nodes declare `target`?
///
/// **Permitted iff both sides declare the same, non-`None` group.** One rule,
/// no special cases.
///
/// The case it exists for is two *different* declared groups — joining a dev Pi
/// into prod is refused rather than trusted, where the only prior guard was a
/// comment in a TOML saying not to. But an undeclared side is refused too, and
/// that is the deliberate half: **`None` means "in no group", not "unknown"**,
/// so growing prod with an unstamped box is exactly as much a cross-group join
/// as the dev case is. Failing open there would leave the operator believing a
/// guarantee that was never evaluated.
///
/// The distinction that word carries matters most at the *node* boundary. A
/// `MachineConfig` with no `sovereign_group` has genuinely declared standalone.
/// A daemon started without `--sovereign-group` has declared nothing — the
/// declaration never reached the box — and a caller that cannot tell those
/// apart must not pass `None` here and read the answer as "standalone". Resolve
/// the unknown first; this function only judges declarations.