Skip to main content

lenso_service/
system_plane.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4use std::collections::{BTreeSet, HashSet};
5use utoipa::ToSchema;
6
7pub mod enrollment;
8pub mod module_operations;
9pub mod runtime_observability;
10pub mod runtime_operations;
11
12pub use enrollment::*;
13pub use module_operations::*;
14pub use runtime_observability::*;
15pub use runtime_operations::*;
16
17pub const CORE_PROTOCOL: &str = "lenso.system-plane.v1";
18pub const CORE_PATH: &str = "/system-plane/v1";
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
21#[serde(rename_all = "camelCase", deny_unknown_fields)]
22pub struct CoreDocument {
23    pub protocol: String,
24    #[schema(min_length = 1)]
25    pub service_id: String,
26    #[schema(min_length = 1)]
27    pub service_principal: String,
28    #[schema(min_length = 1)]
29    pub service_revision: String,
30    #[serde(default, skip_serializing_if = "Vec::is_empty")]
31    pub capabilities: Vec<CapabilityAdvertisement>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36pub struct CapabilityAdvertisement {
37    #[schema(pattern = r"^lenso\.system-plane\.[a-z0-9]+(?:[.-][a-z0-9]+)*\.v[1-9][0-9]*$")]
38    pub contract_id: String,
39    #[schema(minimum = 1)]
40    pub major_version: u32,
41    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
42    pub feature_ids: BTreeSet<String>,
43    #[schema(pattern = r"^sha256:[0-9a-f]{64}$")]
44    pub schema_digest: String,
45    #[schema(pattern = r"^/system-plane/v1/[a-z0-9]+(?:[/-][a-z0-9]+)*$")]
46    pub endpoint: String,
47}
48
49#[derive(
50    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
51)]
52#[serde(rename_all = "snake_case")]
53pub enum CoreIssueCode {
54    InvalidProtocol,
55    MissingServiceIdentity,
56    MissingServicePrincipal,
57    MissingServiceRevision,
58    InvalidCapabilityContractId,
59    InvalidCapabilityMajorVersion,
60    CapabilityMajorVersionMismatch,
61    InvalidFeatureId,
62    InvalidSchemaDigest,
63    InvalidEndpointReference,
64    DuplicateCapability,
65}
66
67#[derive(
68    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
69)]
70#[serde(rename_all = "camelCase", deny_unknown_fields)]
71pub struct CoreIssue {
72    pub code: CoreIssueCode,
73    pub path: String,
74    pub message: String,
75    pub next_action: String,
76}
77
78#[must_use]
79pub fn validate_core_document(document: &CoreDocument) -> Vec<CoreIssue> {
80    let mut issues = Vec::new();
81
82    if document.protocol != CORE_PROTOCOL {
83        push_issue(
84            &mut issues,
85            CoreIssueCode::InvalidProtocol,
86            "$.protocol",
87            format!("protocol must be `{CORE_PROTOCOL}`"),
88            "Use the supported System Plane Core Protocol identifier.",
89        );
90    }
91    validate_present(
92        &document.service_id,
93        CoreIssueCode::MissingServiceIdentity,
94        "$.serviceId",
95        "serviceId must identify the managed Service",
96        "Publish the stable logical Service identity.",
97        &mut issues,
98    );
99    validate_present(
100        &document.service_principal,
101        CoreIssueCode::MissingServicePrincipal,
102        "$.servicePrincipal",
103        "servicePrincipal must identify the managed Service authority",
104        "Publish the stable Service Principal independently from endpoint and Workload identity.",
105        &mut issues,
106    );
107    validate_present(
108        &document.service_revision,
109        CoreIssueCode::MissingServiceRevision,
110        "$.serviceRevision",
111        "serviceRevision must identify the advertised Service state",
112        "Publish a stable revision that changes when the Core advertisement changes.",
113        &mut issues,
114    );
115
116    let mut advertised_contracts = HashSet::new();
117    for (index, capability) in document.capabilities.iter().enumerate() {
118        let base = format!("$.capabilities[{index}]");
119        let contract_major = capability_major_version(&capability.contract_id);
120
121        if contract_major.is_none() {
122            push_issue(
123                &mut issues,
124                CoreIssueCode::InvalidCapabilityContractId,
125                format!("{base}.contractId"),
126                "contractId must match `lenso.system-plane.<capability>.v<major>`",
127                "Publish a stable capability-specific System Plane Contract identifier.",
128            );
129        }
130        if capability.major_version == 0 {
131            push_issue(
132                &mut issues,
133                CoreIssueCode::InvalidCapabilityMajorVersion,
134                format!("{base}.majorVersion"),
135                "majorVersion must be greater than zero",
136                "Publish the supported major version for this Capability Contract.",
137            );
138        } else if contract_major.is_some_and(|major| major != capability.major_version) {
139            push_issue(
140                &mut issues,
141                CoreIssueCode::CapabilityMajorVersionMismatch,
142                format!("{base}.majorVersion"),
143                "majorVersion must match the version suffix in contractId",
144                "Make the advertised major version and Contract identifier agree.",
145            );
146        }
147
148        for feature_id in &capability.feature_ids {
149            if !valid_dotted_id(feature_id) {
150                push_issue(
151                    &mut issues,
152                    CoreIssueCode::InvalidFeatureId,
153                    format!("{base}.featureIds"),
154                    format!("feature identifier `{feature_id}` is not canonical"),
155                    "Use lowercase dot-separated identifiers with alphanumeric or hyphenated segments.",
156                );
157            }
158        }
159        if !valid_sha256_digest(&capability.schema_digest) {
160            push_issue(
161                &mut issues,
162                CoreIssueCode::InvalidSchemaDigest,
163                format!("{base}.schemaDigest"),
164                "schemaDigest must be a lowercase `sha256:<64 hex>` digest",
165                "Publish the digest of the exact Capability Contract schema.",
166            );
167        }
168        if !valid_capability_endpoint(&capability.endpoint) {
169            push_issue(
170                &mut issues,
171                CoreIssueCode::InvalidEndpointReference,
172                format!("{base}.endpoint"),
173                "endpoint must be a relative subpath below `/system-plane/v1/`",
174                "Publish a capability endpoint inside the managed Service System Plane namespace.",
175            );
176        }
177        if !advertised_contracts.insert(capability.contract_id.as_str()) {
178            push_issue(
179                &mut issues,
180                CoreIssueCode::DuplicateCapability,
181                format!("{base}.contractId"),
182                format!(
183                    "Capability Contract `{}` is advertised more than once",
184                    capability.contract_id
185                ),
186                "Publish each exact Capability Contract identity once.",
187            );
188        }
189    }
190
191    issues
192}
193
194#[must_use]
195pub fn core_document_schema() -> Value {
196    let mut schema = serde_json::to_value(schemars::schema_for!(CoreDocument))
197        .expect("System Plane Core schema serializes");
198    schema["$id"] = Value::String(
199        "https://contracts.lenso.local/system-plane/lenso.system-plane.v1.schema.json".to_owned(),
200    );
201    schema["title"] = Value::String("Lenso System Plane Core Document".to_owned());
202    schema["properties"]["protocol"] = json!({ "const": CORE_PROTOCOL });
203    for field in ["serviceId", "servicePrincipal", "serviceRevision"] {
204        schema["properties"][field]["minLength"] = json!(1);
205    }
206    schema["$defs"]["CapabilityAdvertisement"]["properties"]["contractId"]["pattern"] =
207        json!(r"^lenso\.system-plane\.[a-z0-9]+(?:[.-][a-z0-9]+)*\.v[1-9][0-9]*$");
208    schema["$defs"]["CapabilityAdvertisement"]["properties"]["majorVersion"]["minimum"] = json!(1);
209    schema["$defs"]["CapabilityAdvertisement"]["properties"]["featureIds"]["items"] = json!({
210        "type": "string",
211        "pattern": r"^[a-z0-9]+(?:[.-][a-z0-9]+)*$"
212    });
213    schema["$defs"]["CapabilityAdvertisement"]["properties"]["schemaDigest"]["pattern"] =
214        json!(r"^sha256:[0-9a-f]{64}$");
215    schema["$defs"]["CapabilityAdvertisement"]["properties"]["endpoint"]["pattern"] =
216        json!(r"^/system-plane/v1/[a-z0-9]+(?:[/-][a-z0-9]+)*$");
217    schema
218}
219
220fn validate_present(
221    value: &str,
222    code: CoreIssueCode,
223    path: &str,
224    message: &str,
225    next_action: &str,
226    issues: &mut Vec<CoreIssue>,
227) {
228    if value.trim().is_empty() {
229        push_issue(issues, code, path, message, next_action);
230    }
231}
232
233fn capability_major_version(contract_id: &str) -> Option<u32> {
234    let remainder = contract_id.strip_prefix("lenso.system-plane.")?;
235    let (capability, version) = remainder.rsplit_once(".v")?;
236    if !valid_dotted_id(capability) {
237        return None;
238    }
239    let major = version.parse::<u32>().ok()?;
240    (major > 0 && version == major.to_string()).then_some(major)
241}
242
243fn valid_dotted_id(value: &str) -> bool {
244    !value.is_empty() && value.split('.').all(valid_id_segment)
245}
246
247fn valid_id_segment(segment: &str) -> bool {
248    !segment.is_empty()
249        && !segment.starts_with('-')
250        && !segment.ends_with('-')
251        && !segment.contains("--")
252        && segment
253            .bytes()
254            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
255}
256
257fn valid_sha256_digest(value: &str) -> bool {
258    value.strip_prefix("sha256:").is_some_and(|digest| {
259        digest.len() == 64
260            && digest
261                .bytes()
262                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
263    })
264}
265
266fn valid_capability_endpoint(value: &str) -> bool {
267    value
268        .strip_prefix("/system-plane/v1/")
269        .is_some_and(|suffix| {
270            !suffix.is_empty()
271                && !suffix.starts_with('/')
272                && !suffix.contains("//")
273                && !suffix.contains(['?', '#'])
274                && suffix.split('/').all(valid_id_segment)
275        })
276}
277
278fn push_issue(
279    issues: &mut Vec<CoreIssue>,
280    code: CoreIssueCode,
281    path: impl Into<String>,
282    message: impl Into<String>,
283    next_action: impl Into<String>,
284) {
285    issues.push(CoreIssue {
286        code,
287        path: path.into(),
288        message: message.into(),
289        next_action: next_action.into(),
290    });
291}