1use serde::{Deserialize, Serialize};
8
9pub const DEPLOYMENT_PROFILE_SCHEMA_VERSION: u16 = 1;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum DeploymentProfileKind {
15 OrgGatewayBase,
18 TeamControlBase,
21 AiValueGate,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
28pub struct ProfileFeatureSet {
29 pub auth: bool,
30 pub postgres_store: bool,
31 pub local_views: bool,
32 pub rbac: bool,
33 pub aggregation: bool,
34 pub webhooks: bool,
35 pub connectors: bool,
36 pub sso_scim: bool,
37 pub org_policy: bool,
38 pub assurance: bool,
39 pub settlement: bool,
40}
41
42impl ProfileFeatureSet {
43 #[must_use]
44 pub fn for_profile(kind: DeploymentProfileKind) -> Self {
45 match kind {
46 DeploymentProfileKind::OrgGatewayBase => Self {
47 auth: true,
48 postgres_store: true,
49 local_views: true,
50 rbac: false,
51 aggregation: false,
52 webhooks: false,
53 connectors: false,
54 sso_scim: false,
55 org_policy: false,
56 assurance: false,
57 settlement: false,
58 },
59 DeploymentProfileKind::TeamControlBase => Self {
60 auth: true,
61 postgres_store: true,
62 local_views: true,
63 rbac: true,
64 aggregation: true,
65 webhooks: true,
66 connectors: true,
67 sso_scim: false,
68 org_policy: false,
69 assurance: false,
70 settlement: false,
71 },
72 DeploymentProfileKind::AiValueGate => Self {
73 auth: true,
74 postgres_store: true,
75 local_views: true,
76 rbac: true,
77 aggregation: true,
78 webhooks: true,
79 connectors: true,
80 sso_scim: true,
81 org_policy: true,
82 assurance: true,
83 settlement: true,
84 },
85 }
86 }
87}
88
89#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
91pub struct DeploymentProfileV1 {
92 pub schema_version: u16,
93 pub kind: DeploymentProfileKind,
94 pub display_name: String,
95 pub features: ProfileFeatureSet,
96 pub is_oss: bool,
97 pub requires_license: bool,
98 pub cli_entry_point: String,
99}
100
101impl DeploymentProfileV1 {
102 #[must_use]
103 pub fn resolve(kind: DeploymentProfileKind) -> Self {
104 match kind {
105 DeploymentProfileKind::OrgGatewayBase => Self {
106 schema_version: DEPLOYMENT_PROFILE_SCHEMA_VERSION,
107 kind,
108 display_name: "Org Gateway Base".into(),
109 features: ProfileFeatureSet::for_profile(kind),
110 is_oss: true,
111 requires_license: false,
112 cli_entry_point: "lean-ctx gateway serve".into(),
113 },
114 DeploymentProfileKind::TeamControlBase => Self {
115 schema_version: DEPLOYMENT_PROFILE_SCHEMA_VERSION,
116 kind,
117 display_name: "Team Control Base".into(),
118 features: ProfileFeatureSet::for_profile(kind),
119 is_oss: true,
120 requires_license: false,
121 cli_entry_point: "lean-ctx team serve".into(),
122 },
123 DeploymentProfileKind::AiValueGate => Self {
124 schema_version: DEPLOYMENT_PROFILE_SCHEMA_VERSION,
125 kind,
126 display_name: "AI Value Gate".into(),
127 features: ProfileFeatureSet::for_profile(kind),
128 is_oss: false,
129 requires_license: true,
130 cli_entry_point: "lean-ctx enterprise serve".into(),
131 },
132 }
133 }
134
135 pub fn validate(&self) -> Result<(), DeploymentProfileError> {
136 if self.schema_version != DEPLOYMENT_PROFILE_SCHEMA_VERSION {
137 return Err(DeploymentProfileError::UnsupportedVersion(
138 self.schema_version,
139 ));
140 }
141 if self.kind == DeploymentProfileKind::AiValueGate && self.is_oss {
142 return Err(DeploymentProfileError::BoundaryViolation(
143 "AI Value Gate cannot be marked as OSS".into(),
144 ));
145 }
146 if !self.is_oss && !self.requires_license {
147 return Err(DeploymentProfileError::BoundaryViolation(
148 "non-OSS profile must require a license".into(),
149 ));
150 }
151 Ok(())
152 }
153}
154
155#[derive(Debug, thiserror::Error)]
156pub enum DeploymentProfileError {
157 #[error("unsupported schema version {0}")]
158 UnsupportedVersion(u16),
159 #[error("OSS/Commercial boundary violation: {0}")]
160 BoundaryViolation(String),
161}
162
163#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn org_gateway_is_oss_and_valid() {
171 let profile = DeploymentProfileV1::resolve(DeploymentProfileKind::OrgGatewayBase);
172 profile.validate().unwrap();
173 assert!(profile.is_oss);
174 assert!(!profile.requires_license);
175 assert_eq!(profile.cli_entry_point, "lean-ctx gateway serve");
176 }
177
178 #[test]
179 fn team_control_is_oss_and_valid() {
180 let profile = DeploymentProfileV1::resolve(DeploymentProfileKind::TeamControlBase);
181 profile.validate().unwrap();
182 assert!(profile.is_oss);
183 assert!(!profile.requires_license);
184 }
185
186 #[test]
187 fn ai_value_gate_is_commercial() {
188 let profile = DeploymentProfileV1::resolve(DeploymentProfileKind::AiValueGate);
189 profile.validate().unwrap();
190 assert!(!profile.is_oss);
191 assert!(profile.requires_license);
192 }
193
194 #[test]
195 fn rejects_oss_value_gate() {
196 let mut profile = DeploymentProfileV1::resolve(DeploymentProfileKind::AiValueGate);
197 profile.is_oss = true;
198 assert!(profile.validate().is_err());
199 }
200
201 #[test]
202 fn feature_sets_are_superset_hierarchy() {
203 let org = ProfileFeatureSet::for_profile(DeploymentProfileKind::OrgGatewayBase);
204 let team = ProfileFeatureSet::for_profile(DeploymentProfileKind::TeamControlBase);
205 let gate = ProfileFeatureSet::for_profile(DeploymentProfileKind::AiValueGate);
206
207 assert!(team.rbac && !org.rbac);
208 assert!(gate.sso_scim && !team.sso_scim);
209 assert!(gate.settlement && !team.settlement);
210 }
211}