Skip to main content

liminal_server/services/templates/
configuration.rs

1//! Legacy arrays and root-policy tables both remain explicit configuration.
2
3use super::TemplatePolicy;
4use crate::config::types::ServiceTemplateConfig;
5use serde::{Deserialize, Deserializer};
6
7/// The new table can carry the legacy read-only records under `configured`.
8#[derive(Clone, Debug, PartialEq, Eq, Default)]
9pub struct TemplateSources {
10    configured: Vec<ServiceTemplateConfig>,
11    /// Absent for the legacy array; record-template proposals then refuse.
12    pub policy: Option<TemplatePolicy>,
13}
14impl TemplateSources {
15    /// Root policy enables the registry even when no configuration template exists.
16    #[must_use]
17    pub fn is_empty(&self) -> bool {
18        self.configured.is_empty() && self.policy.is_none()
19    }
20}
21
22impl<'de> Deserialize<'de> for TemplateSources {
23    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
24        #[derive(Deserialize)]
25        #[serde(deny_unknown_fields)]
26        struct Table {
27            #[serde(default)]
28            configured: Vec<ServiceTemplateConfig>,
29            roots: Vec<std::path::PathBuf>,
30            command_bytes: std::num::NonZeroU64,
31            records: std::num::NonZeroUsize,
32            arguments: std::num::NonZeroUsize,
33            environment_names: std::num::NonZeroUsize,
34            text_bytes: std::num::NonZeroUsize,
35            stop_timeout_ms: std::num::NonZeroU64,
36        }
37        #[derive(Deserialize)]
38        #[serde(untagged)]
39        enum Wire {
40            Legacy(Vec<ServiceTemplateConfig>),
41            Table(Table),
42        }
43        Ok(match Wire::deserialize(deserializer)? {
44            Wire::Legacy(configured) => Self {
45                configured,
46                policy: None,
47            },
48            Wire::Table(table) => Self {
49                configured: table.configured,
50                policy: Some(TemplatePolicy {
51                    roots: table.roots,
52                    command_bytes: table.command_bytes,
53                    records: table.records,
54                    arguments: table.arguments,
55                    environment_names: table.environment_names,
56                    text_bytes: table.text_bytes,
57                    stop_timeout_ms: table.stop_timeout_ms,
58                }),
59            },
60        })
61    }
62}
63impl std::ops::Deref for TemplateSources {
64    type Target = [ServiceTemplateConfig];
65    fn deref(&self) -> &Self::Target {
66        &self.configured
67    }
68}
69impl<'a> IntoIterator for &'a TemplateSources {
70    type Item = &'a ServiceTemplateConfig;
71    type IntoIter = std::slice::Iter<'a, ServiceTemplateConfig>;
72    fn into_iter(self) -> Self::IntoIter {
73        self.configured.iter()
74    }
75}
76impl From<Vec<ServiceTemplateConfig>> for TemplateSources {
77    fn from(configured: Vec<ServiceTemplateConfig>) -> Self {
78        Self {
79            configured,
80            policy: None,
81        }
82    }
83}
84
85/// Reads only local filesystem and configured policy before server admission.
86pub fn collect_errors(config: &crate::config::types::ServerConfig, errors: &mut Vec<String>) {
87    let Some(lys) = config.auth.as_ref().and_then(|auth| auth.lys.as_ref()) else {
88        return;
89    };
90    if let Some(policy) = &config.template_sources().policy {
91        let place = config.templates_place();
92        if policy.roots.is_empty()
93            || policy
94                .roots
95                .iter()
96                .any(|root| !root.is_absolute() || !root.is_dir())
97        {
98            errors.push(format!(
99                "[{place}] roots must name existing absolute directories"
100            ));
101        }
102        if lys.construction.is_none() {
103            errors.push(format!("[{place}] roots require [auth.lys.construction]"));
104        }
105    }
106    if let Some(section) = &lys.construction {
107        if lys.agents.is_none() {
108            errors.push("[auth.lys.construction] requires [auth.lys.agents]".into());
109        }
110        if lys.machine.is_none() {
111            errors.push("[auth.lys.construction] requires [auth.lys.machine]".into());
112        }
113        if !lys.live {
114            errors.push(
115                "[auth.lys.construction] requires auth.lys.live for deliberate drafts".into(),
116            );
117        }
118        if config.persistence_path.is_none() || lys.app_credential_path.is_none() {
119            errors.push(
120                "[auth.lys.construction] requires persistence_path and app_credential_path".into(),
121            );
122        }
123        if section.limits.snapshot_bytes.get() < section.limits.request_bytes.get() {
124            errors
125                .push("auth.lys.construction.limits.snapshot_bytes is below request_bytes".into());
126        }
127        for source in [&section.grants.plan, &section.grants.serve] {
128            if source.is_empty()
129                || source.len() > section.limits.text_bytes.get()
130                || source.chars().any(char::is_control)
131            {
132                errors.push(
133                    "auth.lys.construction.grants must name the machine's source grants".into(),
134                );
135            }
136        }
137    }
138}
139
140impl std::fmt::Debug for ServiceTemplateConfig {
141    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        out.debug_struct("ServiceTemplateConfig")
143            .field("subjects", &self.subjects.len())
144            .field("stop_timeout_ms", &self.stop_timeout_ms)
145            .finish_non_exhaustive()
146    }
147}