liminal-server 0.28.0

Standalone server for the liminal messaging bus
Documentation
//! ACCESS-012 R1: where templates may be written, and what each place needs.
//!
//! `[[templates]]` at the top level is the one place an open exchange (no
//! `[auth]`) can write the services it runs for its seats; a gated exchange
//! may write them there or under `[auth.lys]`, never both, so there is one
//! source of truth. The templates themselves are checked whole by
//! [`crate::config::templates`] from whichever place answers. An open
//! exchange takes the root policy beside `[open.construction]` (ACCESS-015).

use crate::config::types::ServerConfig;

pub(super) fn validate_template_places(config: &ServerConfig, errors: &mut Vec<String>) {
    let lys = config.auth.as_ref().and_then(|auth| auth.lys.as_ref());
    if !config.templates.is_empty() && lys.is_some_and(|lys| !lys.templates.is_empty()) {
        errors.push(
            "templates are written once: at the top level or under [auth.lys], not both".to_owned(),
        );
    }
    if config.templates.is_empty() {
        return;
    }
    if config.auth.is_some() && lys.is_none() {
        errors.push(
            "templates: a gated exchange places services in Lys, write [auth.lys] or remove [auth]"
                .to_owned(),
        );
    }
    if config.auth.is_none()
        && let Some(policy) = &config.templates.policy
    {
        if config.open_construction().is_none() {
            errors.push(
                "templates: roots and the record-template policy need [open.construction] on an \
                 open exchange or [auth.lys.construction] on a gated one"
                    .to_owned(),
            );
        }
        if policy.roots.is_empty()
            || policy
                .roots
                .iter()
                .any(|root| !root.is_absolute() || !root.is_dir())
        {
            errors.push("[templates] roots must name existing absolute directories".to_owned());
        }
    }
}

/// ACCESS-015 R1: what `[open.construction]` needs beside it. `[open]` is
/// already refused beside `[auth]`, so these are an open exchange's alone.
pub(super) fn validate_open_construction(config: &ServerConfig, errors: &mut Vec<String>) {
    let Some(limits) = config.open_construction() else {
        return;
    };
    if config.agents.is_none() {
        errors.push("[open.construction] requires top-level [agents]".to_owned());
    }
    if config.persistence_path.is_none() {
        errors.push("[open.construction] requires persistence_path".to_owned());
    }
    if limits.snapshot_bytes.get() < limits.request_bytes.get() {
        errors.push("open.construction.snapshot_bytes is below request_bytes".to_owned());
    }
}