1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! 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 64 (CON-023 R1): the assistant and CON-024's eight builders are nine, and the
/// console draws up to 64 agents.
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 1024 (CON-023 R1): the console's largest subscription set, 514, doubled, as
/// `LimitsConfig::DEFAULT_MAX_SUBSCRIPTIONS_PER_CONNECTION`.
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: 64,
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: 1024,
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());
}
}