liminal-server 0.24.1

Standalone server for the liminal messaging bus
Documentation
//! Positive bounds for agent identities and retained metadata.

use serde::Deserialize;

use super::error::AgentError;
use crate::config::types::ServerConfig;

/// `[auth.lys.agents]`. An absent block is distinct from a block using defaults.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct AgentsConfig {
    /// Default 8: one person's assistant plus a hand of builders.
    pub max_per_owner: usize,
    /// Default 64: one exchange's console stays readable.
    pub max_per_exchange: usize,
    /// Default 512: identity, purpose and draft identifiers fit without
    /// admitting arbitrarily large strings to the metadata registry.
    pub max_field_bytes: usize,
    /// Default 16 KiB: one transition and its receipt fit in one store request.
    pub max_event_bytes: usize,
    /// Default 64: restoration reads history in bounded pages.
    pub replay_page_rows: usize,
    /// Default 256: metadata watchers cannot grow an unbounded actor population.
    pub max_watchers: usize,
    /// Default 16: one watcher cannot retain an unbounded metadata backlog.
    pub watcher_depth: usize,
    /// Default 256 KiB: non-wire watchers also have a positive byte budget.
    pub watcher_bytes: usize,
    /// Default 32: one seat's graph stops stay within a bounded subscription list.
    pub max_listened_subjects: usize,
    /// Default 4 KiB: each granted subject name has a bounded graph representation.
    pub max_subject_bytes: usize,
}

impl Default for AgentsConfig {
    fn default() -> Self {
        Self {
            max_per_owner: 8,
            max_per_exchange: 64,
            max_field_bytes: 512,
            max_event_bytes: 16 * 1024,
            replay_page_rows: 64,
            max_watchers: 256,
            watcher_depth: 16,
            watcher_bytes: 256 * 1024,
            max_listened_subjects: 32,
            max_subject_bytes: 4096,
        }
    }
}

impl AgentsConfig {
    /// Validates every bound before allocation.
    ///
    /// # Errors
    /// A bound is zero, or an event cannot hold one bounded field.
    pub fn validate(&self) -> Result<(), AgentError> {
        for (name, value) in self.bounds() {
            if value == 0 {
                return Err(AgentError::Bound {
                    bound: name,
                    limit: 0,
                    count: 0,
                });
            }
        }
        if self.max_event_bytes < self.max_field_bytes {
            return Err(AgentError::Invalid(
                "max_event_bytes is below max_field_bytes",
            ));
        }
        Ok(())
    }

    const fn bounds(&self) -> [(&'static str, usize); 10] {
        [
            ("max_per_owner", self.max_per_owner),
            ("max_per_exchange", self.max_per_exchange),
            ("max_field_bytes", self.max_field_bytes),
            ("max_event_bytes", self.max_event_bytes),
            ("replay_page_rows", self.replay_page_rows),
            ("max_watchers", self.max_watchers),
            ("watcher_depth", self.watcher_depth),
            ("watcher_bytes", self.watcher_bytes),
            ("max_listened_subjects", self.max_listened_subjects),
            ("max_subject_bytes", self.max_subject_bytes),
        ]
    }

    pub(crate) fn field(&self, value: &str, member: &'static str) -> Result<(), AgentError> {
        if value.is_empty() || value.chars().any(char::is_control) {
            return Err(AgentError::Invalid(member));
        }
        if value.len() > self.max_field_bytes {
            return Err(AgentError::Bound {
                bound: "max_field_bytes",
                limit: self.max_field_bytes,
                count: value.len(),
            });
        }
        Ok(())
    }
}

/// Collects deterministic configuration errors. A durable registry discovered
/// at boot is checked again when opened, before the listener starts.
pub fn collect_errors(config: &ServerConfig, errors: &mut Vec<String>) {
    let lys = config.auth.as_ref().and_then(|auth| auth.lys.as_ref());
    if let Some(bounds) = lys.and_then(|lys| lys.agents.as_ref()) {
        if let Err(error) = bounds.validate() {
            errors.push(error.to_string());
        }
    } else if config
        .channels
        .iter()
        .any(|channel| channel.name.starts_with("agents."))
        || lys
            .and_then(|lys| lys.membership.as_ref())
            .is_some_and(|membership| membership.channel_kind == "liminal.agent")
    {
        errors.push("[auth.lys.agents] is required when a liminal.agent is placed".into());
    }
}