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.place_own_station(session, channel)?;
209 self.ensure_agent_channel(channel)?;
210 let id = self
211 .next_subscription_id
212 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
213 let subscription = agents
214 .subscribe(
215 crate::agents::runtime::AgentSubscription {
216 services: self,
217 connection,
218 session,
219 subject: channel,
220 schemas,
221 install,
222 id,
223 },
224 |install| {
225 self.open_subscription(channel, schemas, install, None)
226 .map_err(|error| AgentError::Store(error.to_string()))
227 },
228 )
229 .map_err(ServerError::from)?;
230 self.track_agent_listen(connection, channel, subscription)
231 }
232
233 fn track_agent_listen(
234 &self,
235 connection: u64,
236 subject: &str,
237 subscription: ConnectionSubscription,
238 ) -> Result<ConnectionSubscription, ServerError> {
239 let Some(agents) = &self.agents else {
240 return Ok(subscription);
241 };
242 let ConnectionSubscription {
243 id,
244 channel,
245 stream_id,
246 selected_schema,
247 resource,
248 issued,
249 } = subscription;
250 let resource = agents.track_listen(connection, id, subject, resource)?;
251 Ok(ConnectionSubscription {
252 id,
253 channel,
254 stream_id,
255 selected_schema,
256 resource,
257 issued,
258 })
259 }
260
261 pub fn agent_created(&self, name: &str) -> Result<(), ServerError> {
267 self.ensure_agent_channel(&format!("agents.{name}.inbox"))?;
268 self.ensure_agent_channel(&format!("agents.{name}.cancel"))?;
269 self.ensure_agent_channel(&format!("agents.{name}.stream"))?;
270 if let Some(delivery) = &self.agent_delivery {
271 let inbox = self
272 .read_roster()
273 .map_err(|error| AgentError::Store(error.to_string()))?
274 .get(&format!("agents.{name}.inbox"))
275 .map(|channel| channel.handle.clone())
276 .ok_or(AgentError::Invalid("registered inbox absent"))?;
277 delivery.bind_inbox(name, inbox).map_err(draft_error)?;
278 }
279 self.agents
280 .as_ref()
281 .ok_or(AgentError::Invalid("agent registry absent"))?
282 .created(name)?;
283 self.console
284 .emit(crate::console::TopologyChange::Service {
285 subject: format!("agents.{name}.inbox"),
286 present: true,
287 })
288 .map_err(|_| AgentError::Delivery("agent topology change failed"))?;
289 Ok(())
290 }
291
292 fn place_own_station(
297 &self,
298 session: Option<&dyn LysSession>,
299 channel: &str,
300 ) -> Result<(), ServerError> {
301 let (Some(session), Some((name, "inbox")), Some(agents)) =
302 (session, binding::subject(channel), self.agents.as_ref())
303 else {
304 return Ok(());
305 };
306 if session.holder_kind() != Some("agent") || session.holder() != name {
307 return Ok(());
308 }
309 if agents.registry().place_own(name, agents.now()?)? {
310 self.agent_created(name)?;
311 }
312 Ok(())
313 }
314
315 fn ensure_agent_channel(&self, channel: &str) -> Result<(), ServerError> {
316 let (name, leaf) = binding::subject(channel).ok_or(AgentError::Invalid("agent subject"))?;
317 let agents = self
318 .agents
319 .as_ref()
320 .ok_or(AgentError::Invalid("agent registry absent"))?;
321 let record = agents.registry().record(name)?;
322 if matches!(leaf, "presence" | "state") {
323 return Ok(());
324 }
325 if !record.active() {
326 return Err(AgentError::Transition("agent is retired").into());
327 }
328 if !matches!(leaf, "inbox" | "cancel" | "stream") {
329 return Ok(());
330 }
331 let schema = serde_json::to_vec(&serde_json::json!({"type":"object"}))
332 .map_err(|_| AgentError::Invalid("inbox schema encoding"))?;
333 self.register_channel(&ChannelRegistration {
334 name: channel.into(),
335 schema_bytes: (leaf == "inbox").then_some(schema),
336 durable: leaf == "inbox",
337 })
338 .map_err(|error| AgentError::Store(error.to_string()))?;
339 Ok(())
340 }
341
342 pub(super) fn agent_channel_admission(
343 &self,
344 operation: crate::server::connection::ChannelOperation,
345 channel: &str,
346 ) -> Option<Result<(), ChannelAccessError>> {
347 let (name, leaf) = binding::subject(channel)?;
348 if !matches!(leaf, "presence" | "state" | "inbox" | "cancel" | "stream") {
349 return None;
350 }
351 if operation == crate::server::connection::ChannelOperation::Subscribe
355 && leaf == "inbox"
356 && self
357 .agents
358 .as_ref()
359 .is_some_and(|agents| agents.registry().record(name).is_err())
360 {
361 return Some(Ok(()));
362 }
363 Some(self.ensure_agent_channel(channel).map_err(|error| {
364 ChannelAccessError::RosterUnavailable {
365 message: error.to_string(),
366 }
367 }))
368 }
369
370 pub fn agent_topology_payload<'a>(
375 &self,
376 subject: &str,
377 payload: &'a [u8],
378 ) -> Result<std::borrow::Cow<'a, [u8]>, crate::console::ConsoleError> {
379 let Some(agents) = &self.agents else {
380 return Ok(std::borrow::Cow::Borrowed(payload));
381 };
382 if subject != crate::console::TOPOLOGY_SUBJECT {
383 return Ok(std::borrow::Cow::Borrowed(payload));
384 }
385 let bytes = crate::console::topology::with_agents(payload, agents, self.console.device())
386 .map_err(|error| crate::console::ConsoleError::Server {
387 subject: subject.into(),
388 source: error.into(),
389 })?;
390 Ok(std::borrow::Cow::Owned(bytes))
391 }
392}
393
394fn validate_inbox(
395 bounds: &crate::agents::config::AgentsConfig,
396 payload: &[u8],
397) -> Result<(), AgentError> {
398 if payload.len() > bounds.max_event_bytes {
399 return Err(AgentError::Bound {
400 bound: "max_event_bytes",
401 limit: bounds.max_event_bytes,
402 count: payload.len(),
403 });
404 }
405 let value: serde_json::Value =
406 serde_json::from_slice(payload).map_err(|_| AgentError::Invalid("inbox JSON"))?;
407 let control = |value: &serde_json::Value, keys: &[&str]| {
410 value.as_object().is_some_and(|fields| {
411 fields.iter().all(|(key, value)| {
412 keys.contains(&key.as_str()) && (value.is_string() || value.is_boolean())
413 })
414 })
415 };
416 let accepted = value.as_object().is_some_and(|object| {
417 object.len() == 1
418 && (object.get("text").is_some_and(serde_json::Value::is_string)
419 || object
420 .get("config")
421 .is_some_and(|c| control(c, &["option", "value"]))
422 || object
423 .get("command")
424 .is_some_and(|c| control(c, &["name", "input"]))
425 || object
426 .get("session")
427 .is_some_and(|c| control(c, &["list", "new", "open"])))
428 });
429 if !accepted {
430 return Err(AgentError::Invalid(
431 "inbox accepts exactly one of {text}, {command}, {config} or {session}",
432 ));
433 }
434 Ok(())
435}
436
437#[cfg(test)]
438#[path = "server_tests.rs"]
439mod tests;
440
441type AgentOwners = (
442 Option<std::sync::Arc<crate::agents::runtime::Agents>>,
443 Option<std::sync::Arc<crate::agents::waiting::durable::ApprovalAgents>>,
444 Option<std::sync::Arc<crate::agents::waiting::AgentDelivery>>,
445);
446fn draft_error(error: crate::auth_lys::drafts::DraftError) -> ServerError {
447 error.into()
448}
449impl From<crate::auth_lys::drafts::DraftError> for ServerError {
450 fn from(error: crate::auth_lys::drafts::DraftError) -> Self {
451 Self::ConfigValidation {
452 message: error.to_string(),
453 }
454 }
455}