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}