liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! Legacy arrays and root-policy tables both remain explicit configuration.

use super::TemplatePolicy;
use crate::config::types::ServiceTemplateConfig;
use serde::{Deserialize, Deserializer};

/// The new table can carry the legacy read-only records under `configured`.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct TemplateSources {
    configured: Vec<ServiceTemplateConfig>,
    /// Absent for the legacy array; record-template proposals then refuse.
    pub policy: Option<TemplatePolicy>,
}
impl TemplateSources {
    /// Root policy enables the registry even when no configuration template exists.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.configured.is_empty() && self.policy.is_none()
    }
}

impl<'de> Deserialize<'de> for TemplateSources {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        #[serde(deny_unknown_fields)]
        struct Table {
            #[serde(default)]
            configured: Vec<ServiceTemplateConfig>,
            roots: Vec<std::path::PathBuf>,
            command_bytes: std::num::NonZeroU64,
            records: std::num::NonZeroUsize,
            arguments: std::num::NonZeroUsize,
            environment_names: std::num::NonZeroUsize,
            text_bytes: std::num::NonZeroUsize,
            stop_timeout_ms: std::num::NonZeroU64,
        }
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum Wire {
            Legacy(Vec<ServiceTemplateConfig>),
            Table(Table),
        }
        Ok(match Wire::deserialize(deserializer)? {
            Wire::Legacy(configured) => Self {
                configured,
                policy: None,
            },
            Wire::Table(table) => Self {
                configured: table.configured,
                policy: Some(TemplatePolicy {
                    roots: table.roots,
                    command_bytes: table.command_bytes,
                    records: table.records,
                    arguments: table.arguments,
                    environment_names: table.environment_names,
                    text_bytes: table.text_bytes,
                    stop_timeout_ms: table.stop_timeout_ms,
                }),
            },
        })
    }
}
impl std::ops::Deref for TemplateSources {
    type Target = [ServiceTemplateConfig];
    fn deref(&self) -> &Self::Target {
        &self.configured
    }
}
impl<'a> IntoIterator for &'a TemplateSources {
    type Item = &'a ServiceTemplateConfig;
    type IntoIter = std::slice::Iter<'a, ServiceTemplateConfig>;
    fn into_iter(self) -> Self::IntoIter {
        self.configured.iter()
    }
}
impl From<Vec<ServiceTemplateConfig>> for TemplateSources {
    fn from(configured: Vec<ServiceTemplateConfig>) -> Self {
        Self {
            configured,
            policy: None,
        }
    }
}

/// Reads only local filesystem and configured policy before server admission.
pub fn collect_errors(config: &crate::config::types::ServerConfig, errors: &mut Vec<String>) {
    let Some(lys) = config.auth.as_ref().and_then(|auth| auth.lys.as_ref()) else {
        return;
    };
    if let Some(policy) = &lys.templates.policy {
        if policy.roots.is_empty()
            || policy
                .roots
                .iter()
                .any(|root| !root.is_absolute() || !root.is_dir())
        {
            errors
                .push("[auth.lys.templates] roots must name existing absolute directories".into());
        }
        if lys.construction.is_none() {
            errors.push("[auth.lys.templates] roots require [auth.lys.construction]".into());
        }
    }
    if let Some(section) = &lys.construction {
        if lys.agents.is_none() {
            errors.push("[auth.lys.construction] requires [auth.lys.agents]".into());
        }
        if lys.machine.is_none() {
            errors.push("[auth.lys.construction] requires [auth.lys.machine]".into());
        }
        if !lys.live {
            errors.push(
                "[auth.lys.construction] requires auth.lys.live for deliberate drafts".into(),
            );
        }
        if config.persistence_path.is_none() || lys.app_credential_path.is_none() {
            errors.push(
                "[auth.lys.construction] requires persistence_path and app_credential_path".into(),
            );
        }
        if section.limits.snapshot_bytes.get() < section.limits.request_bytes.get() {
            errors
                .push("auth.lys.construction.limits.snapshot_bytes is below request_bytes".into());
        }
        for source in [&section.grants.plan, &section.grants.serve] {
            if source.is_empty()
                || source.len() > section.limits.text_bytes.get()
                || source.chars().any(char::is_control)
            {
                errors.push(
                    "auth.lys.construction.grants must name the machine's source grants".into(),
                );
            }
        }
    }
}

impl std::fmt::Debug for ServiceTemplateConfig {
    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        out.debug_struct("ServiceTemplateConfig")
            .field("subjects", &self.subjects.len())
            .field("stop_timeout_ms", &self.stop_timeout_ms)
            .finish_non_exhaustive()
    }
}