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) = &lys.templates.policy {
91        if policy.roots.is_empty()
92            || policy
93                .roots
94                .iter()
95                .any(|root| !root.is_absolute() || !root.is_dir())
96        {
97            errors
98                .push("[auth.lys.templates] roots must name existing absolute directories".into());
99        }
100        if lys.construction.is_none() {
101            errors.push("[auth.lys.templates] roots require [auth.lys.construction]".into());
102        }
103    }
104    if let Some(section) = &lys.construction {
105        if lys.agents.is_none() {
106            errors.push("[auth.lys.construction] requires [auth.lys.agents]".into());
107        }
108        if lys.machine.is_none() {
109            errors.push("[auth.lys.construction] requires [auth.lys.machine]".into());
110        }
111        if !lys.live {
112            errors.push(
113                "[auth.lys.construction] requires auth.lys.live for deliberate drafts".into(),
114            );
115        }
116        if config.persistence_path.is_none() || lys.app_credential_path.is_none() {
117            errors.push(
118                "[auth.lys.construction] requires persistence_path and app_credential_path".into(),
119            );
120        }
121        if section.limits.snapshot_bytes.get() < section.limits.request_bytes.get() {
122            errors
123                .push("auth.lys.construction.limits.snapshot_bytes is below request_bytes".into());
124        }
125        for source in [&section.grants.plan, &section.grants.serve] {
126            if source.is_empty()
127                || source.len() > section.limits.text_bytes.get()
128                || source.chars().any(char::is_control)
129            {
130                errors.push(
131                    "auth.lys.construction.grants must name the machine's source grants".into(),
132                );
133            }
134        }
135    }
136}
137
138impl std::fmt::Debug for ServiceTemplateConfig {
139    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        out.debug_struct("ServiceTemplateConfig")
141            .field("subjects", &self.subjects.len())
142            .field("stop_timeout_ms", &self.stop_timeout_ms)
143            .finish_non_exhaustive()
144    }
145}