Skip to main content

lean_ctx/core/
deployment_profile.rs

1//! Customer Deployment Profiles (P11).
2//!
3//! Packages the existing gateway_server/ and http_server/team/ into coherent,
4//! named deployment profiles. No new binary is created; these are run-mode
5//! configurations that absorb the existing KMU Gateway and Team Server.
6
7use serde::{Deserialize, Serialize};
8
9pub const DEPLOYMENT_PROFILE_SCHEMA_VERSION: u16 = 1;
10
11/// Available deployment profiles.
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum DeploymentProfileKind {
15    /// OSS, self-hosted, Auth, Postgres, local views.
16    /// Absorbs: `gateway_server/`
17    OrgGatewayBase,
18    /// OSS, RBAC basis, Aggregation, Webhook, Connectors.
19    /// Absorbs: `http_server/team/`
20    TeamControlBase,
21    /// Commercial add-on (NOT in OSS).
22    /// SSO/SCIM, Org Policy, Assurance, Retention, Settlement.
23    AiValueGate,
24}
25
26/// Feature flags available per deployment profile.
27#[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/// A resolved deployment profile with metadata.
90#[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// ─── Tests ───────────────────────────────────────────────────────────────────
164
165#[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}