yah-workload-spec 0.8.26

WorkloadSpec — typed wire format for yubaba workloads. Schema crate with zero deps on yubaba; yubaba depends on this, not the other way around.
Documentation
//! 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.
pub fn join_permitted(joiner: Option<&str>, target: Option<&str>) -> bool {
    matches!((joiner, target), (Some(a), Some(b)) if a == b)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_group_joins_itself() {
        assert!(join_permitted(Some("dev"), Some("dev")));
        assert!(join_permitted(Some("prod"), Some("prod")));
    }

    /// The refusal the field was added for.
    #[test]
    fn two_groups_do_not_merge() {
        assert!(!join_permitted(Some("dev"), Some("prod")));
    }

    /// `None` is a declaration, not a gap — so it never matches, including
    /// against itself. Two unstamped boxes forming a group nobody declared is
    /// the shape that leaves nothing to reason about later.
    #[test]
    fn undeclared_never_joins_anything() {
        assert!(!join_permitted(None, Some("prod")));
        assert!(!join_permitted(Some("dev"), None));
        assert!(!join_permitted(None, None));
    }

    /// Group labels are compared exactly. A `"Dev"`/`"dev"` typo mints a
    /// phantom group rather than silently joining the real one, which is the
    /// safe direction: the refusal names both values, so the typo is visible
    /// at the moment it bites.
    #[test]
    fn labels_are_compared_exactly() {
        assert!(!join_permitted(Some("Dev"), Some("dev")));
        assert!(!join_permitted(Some("dev "), Some("dev")));
    }
}