Skip to main content

yeti_types/
safety.rs

1//! Deployment-safety declarations for toggleable internal crates.
2//!
3//! Every crate that adds a config-gated feature (anything that reads a
4//! top-level section from yeti-config.yaml and may `enabled: true/false`)
5//! exports a `pub const SAFETY: SafetyProfile` and submits it to the
6//! inventory registry so:
7//!
8//!   * `yeti config lint` can validate config files against a deployment
9//!     profile (local / fabric / self-host).
10//!   * `yeti-feature-matrix` can render a human-readable capability table
11//!     as `docs/reference/feature-matrix.md`.
12//!   * CI can diff the generated matrix against the checked-in file and
13//!     fail the PR if they drift.
14//!
15//! The crate's `Cargo.toml` may additionally be excluded from the
16//! `fabric` cargo feature set, which is the strongest layer — a
17//! fabric-excluded crate isn't in the binary at all.
18
19/// Deployment-safety metadata for one toggleable crate or sub-feature.
20///
21/// Profiles are declared as `const` and collected via
22/// `inventory::collect!`. At runtime the linter and matrix-generator
23/// walk the registry and check each entry against the target deployment
24/// profile (see `DeploymentProfile`).
25#[derive(Debug, Clone, Copy)]
26pub struct SafetyProfile {
27    /// Config section / feature name this profile guards. Matches the
28    /// top-level yeti-config.yaml key (e.g. `"workspace"`, `"ratelimit"`).
29    /// Sub-features use a qualified key (e.g. `"admin.files"`).
30    pub name: &'static str,
31
32    /// Whether the crate may be enabled on multi-tenant Fabric
33    /// deployments. `false` means the crate's functionality is unsafe
34    /// in a shared-infrastructure context (typically because it grants
35    /// ambient OS-level authority that cgroups alone can't contain).
36    pub fabric_allowed: bool,
37
38    /// Default `enabled:` state when the section is absent from config.
39    /// `false` for anything fabric-unsafe (safe-by-default). `true` for
40    /// always-on platform primitives (auth, telemetry).
41    pub default_enabled: bool,
42
43    /// Human-readable explanation. Shown in the feature matrix and in
44    /// lint errors. Keep it short and answer "why is this the default?"
45    pub rationale: &'static str,
46}
47
48/// Which deployment profile a config is being validated against.
49///
50/// Profiles differ in what they permit:
51///   * `Local` — a developer's workstation. All crates permitted; the
52///     operator IS the trust boundary.
53///   * `SelfHost` — a single customer's self-hosted server. Defaults to
54///     the same permissions as `Local`; operators can tighten by
55///     providing their own profile override YAML.
56///   * `Fabric` — multi-tenant cloud. Only `fabric_allowed: true`
57///     crates may be enabled.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum DeploymentProfile {
60    /// Developer's local workstation. Permissive.
61    Local,
62    /// Single-tenant self-hosted deployment. Operator-trusted.
63    SelfHost,
64    /// Multi-tenant shared infrastructure. Restricted.
65    Fabric,
66}
67
68impl DeploymentProfile {
69    /// Parse a profile from a string (as passed to `yeti config lint
70    /// --profile <name>`).
71    #[must_use]
72    pub fn from_name(name: &str) -> Option<Self> {
73        match name {
74            "local" => Some(Self::Local),
75            "self-host" | "selfhost" => Some(Self::SelfHost),
76            "fabric" => Some(Self::Fabric),
77            _ => None,
78        }
79    }
80
81    /// The name shown in CLI help + lint error output.
82    #[must_use]
83    pub const fn name(&self) -> &'static str {
84        match self {
85            Self::Local => "local",
86            Self::SelfHost => "self-host",
87            Self::Fabric => "fabric",
88        }
89    }
90}
91
92impl SafetyProfile {
93    /// Whether this crate may be enabled under the given deployment
94    /// profile. Used by `yeti config lint`.
95    #[must_use]
96    pub const fn allowed_under(&self, profile: DeploymentProfile) -> bool {
97        match profile {
98            DeploymentProfile::Fabric => self.fabric_allowed,
99            // Local + SelfHost permit everything; the operator owns the
100            // trust boundary. If a self-host operator wants to tighten,
101            // they run `yeti config lint --profile fabric` as a stricter
102            // gate.
103            DeploymentProfile::Local | DeploymentProfile::SelfHost => true,
104        }
105    }
106}
107
108// Inventory registration — every toggleable crate calls
109// `inventory::submit! { SAFETY }` at the top level. The linter and
110// matrix-generator iterate via `inventory::iter::<SafetyProfile>`.
111inventory::collect!(SafetyProfile);
112
113/// Iterate every registered `SafetyProfile`. Returned in unspecified
114/// order; callers that need deterministic output (e.g. matrix-generator)
115/// should sort by `name`.
116pub fn registered_profiles() -> impl Iterator<Item = &'static SafetyProfile> {
117    inventory::iter::<SafetyProfile>()
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn deployment_profile_from_name_round_trip() {
126        for profile in [
127            DeploymentProfile::Local,
128            DeploymentProfile::SelfHost,
129            DeploymentProfile::Fabric,
130        ] {
131            assert_eq!(DeploymentProfile::from_name(profile.name()), Some(profile));
132        }
133    }
134
135    #[test]
136    fn deployment_profile_from_name_accepts_aliases() {
137        // The `self-host` canonical form + the `selfhost` alias both parse.
138        assert_eq!(
139            DeploymentProfile::from_name("selfhost"),
140            Some(DeploymentProfile::SelfHost)
141        );
142        assert_eq!(
143            DeploymentProfile::from_name("self-host"),
144            Some(DeploymentProfile::SelfHost)
145        );
146    }
147
148    #[test]
149    fn deployment_profile_from_name_rejects_unknown() {
150        assert_eq!(DeploymentProfile::from_name("production"), None);
151        assert_eq!(DeploymentProfile::from_name(""), None);
152    }
153
154    #[test]
155    fn allowed_under_fabric_respects_flag() {
156        let unsafe_for_fabric = SafetyProfile {
157            name: "test-unsafe",
158            fabric_allowed: false,
159            default_enabled: false,
160            rationale: "",
161        };
162        let safe_for_fabric = SafetyProfile {
163            name: "test-safe",
164            fabric_allowed: true,
165            default_enabled: true,
166            rationale: "",
167        };
168        assert!(!unsafe_for_fabric.allowed_under(DeploymentProfile::Fabric));
169        assert!(safe_for_fabric.allowed_under(DeploymentProfile::Fabric));
170    }
171
172    #[test]
173    fn allowed_under_local_and_self_host_permits_all() {
174        let unsafe_for_fabric = SafetyProfile {
175            name: "test",
176            fabric_allowed: false,
177            default_enabled: false,
178            rationale: "",
179        };
180        assert!(unsafe_for_fabric.allowed_under(DeploymentProfile::Local));
181        assert!(unsafe_for_fabric.allowed_under(DeploymentProfile::SelfHost));
182    }
183}