1use super::{
4 ChannelAccessError, ChannelRegistration, ConnectionServices, ConnectionSubscription,
5 LiminalConnectionServices, ProtocolSchemaId, ServerError,
6};
7use crate::agents::{binding, error::AgentError};
8use crate::auth_lys::session::LysSession;
9
10impl LiminalConnectionServices {
11 pub(super) fn configured_agents(
12 config: &crate::config::ServerConfig,
13 store: std::sync::Arc<dyn liminal::durability::DurableStore>,
14 supervisor: liminal::channel::ChannelSupervisor,
15 console: &std::sync::Arc<crate::console::ConsoleHub>,
16 runtime: Option<tokio::runtime::Handle>,
17 ) -> Result<AgentOwners, ServerError> {
18 let lys = config.auth.as_ref().and_then(|auth| auth.lys.as_ref());
19 let (agents, owners, delivery) = if let Some((lys, bounds, limits)) =
20 lys.and_then(|lys| Some((lys, lys.agents?, lys.drafts?)))
21 {
22 let (placer, seats) = crate::agents::effects::configured(lys, runtime)?;
23 let bindings = crate::agents::waiting::durable::AgentBindings {
24 bounds,
25 network: lys.network_id.clone(),
26 supervisor,
27 device: console.device().into(),
28 placer,
29 seats,
30 };
31 let owners = crate::agents::waiting::durable::ApprovalAgents::open(
32 store,
33 limits.journal,
34 bindings,
35 )
36 .map_err(draft_error)?;
37 let delivery = crate::agents::waiting::AgentDelivery::new(
38 &owners,
39 std::collections::BTreeMap::new(),
40 format!("liminal.{}", console.device()),
41 limits.waiting,
42 )
43 .map_err(draft_error)?;
44 (Some(owners.agents().clone()), Some(owners), Some(delivery))
45 } else {
46 (
47 crate::agents::runtime::configured(
48 config,
49 store,
50 supervisor,
51 console.device(),
52 runtime,
53 )?,
54 None,
55 None,
56 )
57 };
58 if let Some(agents) = &agents {
59 agents.bind_console(console)?;
60 }
61 Ok((agents, owners, delivery))
62 }
63 pub(super) fn publish_agent(
64 &self,
65 connection: u64,
66 session: Option<&dyn LysSession>,
67 channel: &str,
68 envelope: &liminal::protocol::MessageEnvelope,
69 key: Option<&str>,
70 ) -> Result<super::PublishOutcome, ServerError> {
71 binding::check(session, crate::auth_lys::HotAction::Publish, channel)?;
72 if let Some(session) = session.filter(|session| session.holder_kind() == Some("agent")) {
73 self.console
74 .tap()
75 .name_station(connection, session.holder())
76 .map_err(|_| AgentError::Delivery("publisher identity projection failed"))?;
77 }
78 let Some((name, leaf)) = binding::subject(channel) else {
79 return ConnectionServices::publish(self, connection, channel, envelope, key);
80 };
81 let agents = self
82 .agents
83 .as_ref()
84 .ok_or(AgentError::Invalid("agent registry absent"))?;
85 binding::decide(
86 session,
87 agents.network(),
88 crate::auth_lys::HotAction::Publish,
89 channel,
90 agents.now()?,
91 )?;
92 if !agents.registry().record(name)?.active() {
93 return Err(AgentError::Transition("agent is retired").into());
94 }
95 if leaf == "inbox" {
96 validate_inbox(agents.bounds(), &envelope.payload)?;
97 self.ensure_agent_channel(channel)?;
98 }
99 self.publish_with_origin(
100 channel,
101 envelope,
102 key,
103 crate::server::self_streams::events::PublishOrigin::Connection { id: connection },
104 )
105 .map(|publication| publication.outcome)
106 }
107
108 pub fn execute_agent_draft(
115 &self,
116 draft: &lys_pass::drafts::ApprovedDraft,
117 ) -> Result<lys_pass::drafts::Execution, ServerError> {
118 let agents = self
119 .agents
120 .as_ref()
121 .ok_or(AgentError::Invalid("agent registry absent"))?;
122 let receipt = agents.execute_approved(draft)?;
123 match draft.target.action.as_str() {
124 "create_agent" => self.agent_created(&draft.target.id)?,
125 "retire" => self.retire_agent_inbox(&draft.target.id)?,
126 _ => {}
127 }
128 Ok(receipt)
129 }
130
131 fn retire_agent_inbox(&self, name: &str) -> Result<(), ServerError> {
132 let inbox = format!("agents.{name}.inbox");
133 let configured = self
134 .read_roster()
135 .map_err(|error| AgentError::Store(error.to_string()))?
136 .get(&inbox)
137 .cloned();
138 if let Some(configured) = configured {
139 self.quiesce_channel(&inbox, "agent_retired")
140 .map_err(|error| AgentError::Store(error.to_string()))?;
141 configured
142 .handle
143 .close()
144 .map_err(|_| AgentError::Delivery("retired inbox close failed"))?;
145 self.write_roster()
146 .map_err(|error| AgentError::Store(error.to_string()))?
147 .remove(&inbox);
148 }
149 self.console
150 .emit(crate::console::TopologyChange::Service {
151 subject: inbox,
152 present: false,
153 })
154 .map_err(|_| AgentError::Delivery("agent retirement topology failed"))?;
155 Ok(())
156 }
157 #[cfg(any(test, feature = "test-support"))]
162 pub fn with_agent_fixture(
163 mut self,
164 network: &str,
165 placer: std::sync::Arc<dyn crate::services::lifecycle::Placer>,
166 ) -> Result<Self, ServerError> {
167 self.agents = Some(crate::agents::runtime::Agents::open(
168 self.durable_store(),
169 crate::agents::config::AgentsConfig::default(),
170 network.into(),
171 self.cluster.supervisor().clone(),
172 self.console.device(),
173 Some(placer),
174 None,
175 )?);
176 self.agents
177 .as_ref()
178 .ok_or(AgentError::Invalid("agent fixture missing"))?
179 .bind_console(&self.console)?;
180 Ok(self)
181 }
182 #[cfg(any(test, feature = "test-support"))]
186 #[must_use]
187 pub fn with_console_tap_capacity(self, capacity: usize) -> Self {
188 self.console.set_tap_capacity(capacity);
189 self
190 }
191 pub(super) fn subscribe_agent(
192 &self,
193 connection: u64,
194 session: Option<&dyn LysSession>,
195 channel: &str,
196 schemas: &[ProtocolSchemaId],
197 install: Option<liminal::channel::InboxInstall>,
198 ) -> Result<ConnectionSubscription, ServerError> {
199 binding::check(session, crate::auth_lys::HotAction::Subscribe, channel)?;
200 if binding::subject(channel).is_none() {
201 let subscription = ConnectionServices::subscribe(self, channel, schemas, install)?;
202 return self.track_agent_listen(connection, channel, subscription);
203 }
204 let agents = self
205 .agents
206 .as_ref()
207 .ok_or(AgentError::Invalid("[auth.lys.agents] is not configured"))?;
208 self.ensure_agent_channel(channel)?;
209 let id = self
210 .next_subscription_id
211 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
212 let subscription = agents
213 .subscribe(
214 crate::agents::runtime::AgentSubscription {
215 services: self,
216 connection,
217 session,
218 subject: channel,
219 schemas,
220 install,
221 id,
222 },
223 |install| {
224 self.open_subscription(channel, schemas, install, None)
225 .map_err(|error| AgentError::Store(error.to_string()))
226 },
227 )
228 .map_err(ServerError::from)?;
229 self.track_agent_listen(connection, channel, subscription)
230 }
231
232 fn track_agent_listen(
233 &self,
234 connection: u64,
235 subject: &str,
236 subscription: ConnectionSubscription,
237 ) -> Result<ConnectionSubscription, ServerError> {
238 let Some(agents) = &self.agents else {
239 return Ok(subscription);
240 };
241 let ConnectionSubscription {
242 id,
243 channel,
244 stream_id,
245 selected_schema,
246 resource,
247 issued,
248 } = subscription;
249 let resource = agents.track_listen(connection, id, subject, resource)?;
250 Ok(ConnectionSubscription {
251 id,
252 channel,
253 stream_id,
254 selected_schema,
255 resource,
256 issued,
257 })
258 }
259
260 pub fn agent_created(&self, name: &str) -> Result<(), ServerError> {
266 self.ensure_agent_channel(&format!("agents.{name}.inbox"))?;
267 self.ensure_agent_channel(&format!("agents.{name}.cancel"))?;
268 self.ensure_agent_channel(&format!("agents.{name}.stream"))?;
269 if let Some(delivery) = &self.agent_delivery {
270 let inbox = self
271 .read_roster()
272 .map_err(|error| AgentError::Store(error.to_string()))?
273 .get(&format!("agents.{name}.inbox"))
274 .map(|channel| channel.handle.clone())
275 .ok_or(AgentError::Invalid("registered inbox absent"))?;
276 delivery.bind_inbox(name, inbox).map_err(draft_error)?;
277 }
278 self.agents
279 .as_ref()
280 .ok_or(AgentError::Invalid("agent registry absent"))?
281 .created(name)?;
282 self.console
283 .emit(crate::console::TopologyChange::Service {
284 subject: format!("agents.{name}.inbox"),
285 present: true,
286 })
287 .map_err(|_| AgentError::Delivery("agent topology change failed"))?;
288 Ok(())
289 }
290
291 fn ensure_agent_channel(&self, channel: &str) -> Result<(), ServerError> {
292 let (name, leaf) = binding::subject(channel).ok_or(AgentError::Invalid("agent subject"))?;
293 let agents = self
294 .agents
295 .as_ref()
296 .ok_or(AgentError::Invalid("agent registry absent"))?;
297 let record = agents.registry().record(name)?;
298 if matches!(leaf, "presence" | "state") {
299 return Ok(());
300 }
301 if !record.active() {
302 return Err(AgentError::Transition("agent is retired").into());
303 }
304 if !matches!(leaf, "inbox" | "cancel" | "stream") {
305 return Ok(());
306 }
307 let schema = serde_json::to_vec(&serde_json::json!({"type":"object"}))
308 .map_err(|_| AgentError::Invalid("inbox schema encoding"))?;
309 self.register_channel(&ChannelRegistration {
310 name: channel.into(),
311 schema_bytes: (leaf == "inbox").then_some(schema),
312 durable: leaf == "inbox",
313 })
314 .map_err(|error| AgentError::Store(error.to_string()))?;
315 Ok(())
316 }
317
318 pub(super) fn agent_channel_admission(
319 &self,
320 channel: &str,
321 ) -> Option<Result<(), ChannelAccessError>> {
322 let (_, leaf) = binding::subject(channel)?;
323 if !matches!(leaf, "presence" | "state" | "inbox" | "cancel" | "stream") {
324 return None;
325 }
326 Some(self.ensure_agent_channel(channel).map_err(|error| {
327 ChannelAccessError::RosterUnavailable {
328 message: error.to_string(),
329 }
330 }))
331 }
332
333 pub fn agent_topology_payload<'a>(
338 &self,
339 subject: &str,
340 payload: &'a [u8],
341 ) -> Result<std::borrow::Cow<'a, [u8]>, crate::console::ConsoleError> {
342 let Some(agents) = &self.agents else {
343 return Ok(std::borrow::Cow::Borrowed(payload));
344 };
345 if subject != crate::console::TOPOLOGY_SUBJECT {
346 return Ok(std::borrow::Cow::Borrowed(payload));
347 }
348 let bytes = crate::console::topology::with_agents(payload, agents, self.console.device())
349 .map_err(|error| crate::console::ConsoleError::Server {
350 subject: subject.into(),
351 source: error.into(),
352 })?;
353 Ok(std::borrow::Cow::Owned(bytes))
354 }
355}
356
357fn validate_inbox(
358 bounds: &crate::agents::config::AgentsConfig,
359 payload: &[u8],
360) -> Result<(), AgentError> {
361 if payload.len() > bounds.max_event_bytes {
362 return Err(AgentError::Bound {
363 bound: "max_event_bytes",
364 limit: bounds.max_event_bytes,
365 count: payload.len(),
366 });
367 }
368 let value: serde_json::Value =
369 serde_json::from_slice(payload).map_err(|_| AgentError::Invalid("inbox JSON"))?;
370 let control = |value: &serde_json::Value, keys: &[&str]| {
373 value.as_object().is_some_and(|fields| {
374 fields.iter().all(|(key, value)| {
375 keys.contains(&key.as_str()) && (value.is_string() || value.is_boolean())
376 })
377 })
378 };
379 let accepted = value.as_object().is_some_and(|object| {
380 object.len() == 1
381 && (object.get("text").is_some_and(serde_json::Value::is_string)
382 || object
383 .get("config")
384 .is_some_and(|c| control(c, &["option", "value"]))
385 || object
386 .get("command")
387 .is_some_and(|c| control(c, &["name", "input"]))
388 || object
389 .get("session")
390 .is_some_and(|c| control(c, &["list", "new", "open"])))
391 });
392 if !accepted {
393 return Err(AgentError::Invalid(
394 "inbox accepts exactly one of {text}, {command}, {config} or {session}",
395 ));
396 }
397 Ok(())
398}
399
400#[cfg(test)]
401#[path = "server_tests.rs"]
402mod tests;
403
404type AgentOwners = (
405 Option<std::sync::Arc<crate::agents::runtime::Agents>>,
406 Option<std::sync::Arc<crate::agents::waiting::durable::ApprovalAgents>>,
407 Option<std::sync::Arc<crate::agents::waiting::AgentDelivery>>,
408);
409fn draft_error(error: crate::auth_lys::drafts::DraftError) -> ServerError {
410 error.into()
411}
412impl From<crate::auth_lys::drafts::DraftError> for ServerError {
413 fn from(error: crate::auth_lys::drafts::DraftError) -> Self {
414 Self::ConfigValidation {
415 message: error.to_string(),
416 }
417 }
418}