Skip to main content

liminal_server/agents/
config.rs

1//! Positive bounds for agent identities and retained metadata.
2
3use serde::Deserialize;
4
5use super::error::AgentError;
6use crate::config::types::ServerConfig;
7
8/// `[auth.lys.agents]`. An absent block is distinct from a block using defaults.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
10#[serde(default, deny_unknown_fields)]
11pub struct AgentsConfig {
12    /// Default 64 (CON-023 R1): the assistant and CON-024's eight builders are nine, and the
13    /// console draws up to 64 agents.
14    pub max_per_owner: usize,
15    /// Default 64: one exchange's console stays readable.
16    pub max_per_exchange: usize,
17    /// Default 512: identity, purpose and draft identifiers fit without
18    /// admitting arbitrarily large strings to the metadata registry.
19    pub max_field_bytes: usize,
20    /// Default 16 KiB: one transition and its receipt fit in one store request.
21    pub max_event_bytes: usize,
22    /// Default 64: restoration reads history in bounded pages.
23    pub replay_page_rows: usize,
24    /// Default 256: metadata watchers cannot grow an unbounded actor population.
25    pub max_watchers: usize,
26    /// Default 16: one watcher cannot retain an unbounded metadata backlog.
27    pub watcher_depth: usize,
28    /// Default 256 KiB: non-wire watchers also have a positive byte budget.
29    pub watcher_bytes: usize,
30    /// Default 1024 (CON-023 R1): the console's largest subscription set, 514, doubled, as
31    /// `LimitsConfig::DEFAULT_MAX_SUBSCRIPTIONS_PER_CONNECTION`.
32    pub max_listened_subjects: usize,
33    /// Default 4 KiB: each granted subject name has a bounded graph representation.
34    pub max_subject_bytes: usize,
35}
36
37impl Default for AgentsConfig {
38    fn default() -> Self {
39        Self {
40            max_per_owner: 64,
41            max_per_exchange: 64,
42            max_field_bytes: 512,
43            max_event_bytes: 16 * 1024,
44            replay_page_rows: 64,
45            max_watchers: 256,
46            watcher_depth: 16,
47            watcher_bytes: 256 * 1024,
48            max_listened_subjects: 1024,
49            max_subject_bytes: 4096,
50        }
51    }
52}
53
54impl AgentsConfig {
55    /// Validates every bound before allocation.
56    ///
57    /// # Errors
58    /// A bound is zero, or an event cannot hold one bounded field.
59    pub fn validate(&self) -> Result<(), AgentError> {
60        for (name, value) in self.bounds() {
61            if value == 0 {
62                return Err(AgentError::Bound {
63                    bound: name,
64                    limit: 0,
65                    count: 0,
66                });
67            }
68        }
69        if self.max_event_bytes < self.max_field_bytes {
70            return Err(AgentError::Invalid(
71                "max_event_bytes is below max_field_bytes",
72            ));
73        }
74        Ok(())
75    }
76
77    const fn bounds(&self) -> [(&'static str, usize); 10] {
78        [
79            ("max_per_owner", self.max_per_owner),
80            ("max_per_exchange", self.max_per_exchange),
81            ("max_field_bytes", self.max_field_bytes),
82            ("max_event_bytes", self.max_event_bytes),
83            ("replay_page_rows", self.replay_page_rows),
84            ("max_watchers", self.max_watchers),
85            ("watcher_depth", self.watcher_depth),
86            ("watcher_bytes", self.watcher_bytes),
87            ("max_listened_subjects", self.max_listened_subjects),
88            ("max_subject_bytes", self.max_subject_bytes),
89        ]
90    }
91
92    pub(crate) fn field(&self, value: &str, member: &'static str) -> Result<(), AgentError> {
93        if value.is_empty() || value.chars().any(char::is_control) {
94            return Err(AgentError::Invalid(member));
95        }
96        if value.len() > self.max_field_bytes {
97            return Err(AgentError::Bound {
98                bound: "max_field_bytes",
99                limit: self.max_field_bytes,
100                count: value.len(),
101            });
102        }
103        Ok(())
104    }
105}
106
107/// Collects deterministic configuration errors. A durable registry discovered
108/// at boot is checked again when opened, before the listener starts.
109pub fn collect_errors(config: &ServerConfig, errors: &mut Vec<String>) {
110    let lys = config.auth.as_ref().and_then(|auth| auth.lys.as_ref());
111    if let Some(bounds) = lys.and_then(|lys| lys.agents.as_ref()) {
112        if let Err(error) = bounds.validate() {
113            errors.push(error.to_string());
114        }
115    } else if config
116        .channels
117        .iter()
118        .any(|channel| channel.name.starts_with("agents."))
119        || lys
120            .and_then(|lys| lys.membership.as_ref())
121            .is_some_and(|membership| membership.channel_kind == "liminal.agent")
122    {
123        errors.push("[auth.lys.agents] is required when a liminal.agent is placed".into());
124    }
125}