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(crate::agents::error::NO_REGISTRY))?;
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 check_sender(&envelope.payload, session.map(LysSession::holder))?;
98 self.ensure_agent_channel(channel)?;
99 }
100 self.publish_with_origin(
101 channel,
102 envelope,
103 key,
104 crate::server::self_streams::events::PublishOrigin::Connection { id: connection },
105 )
106 .map(|publication| publication.outcome)
107 }
108
109 pub fn execute_agent_draft(
116 &self,
117 draft: &lys_pass::drafts::ApprovedDraft,
118 ) -> Result<lys_pass::drafts::Execution, ServerError> {
119 let agents = self
120 .agents
121 .as_ref()
122 .ok_or(AgentError::Invalid(crate::agents::error::NO_REGISTRY))?;
123 let receipt = agents.execute_approved(draft)?;
124 match draft.target.action.as_str() {
125 "create_agent" => self.agent_created(&draft.target.id)?,
126 "retire" => self.retire_agent_inbox(&draft.target.id)?,
127 _ => {}
128 }
129 Ok(receipt)
130 }
131
132 fn retire_agent_inbox(&self, name: &str) -> Result<(), ServerError> {
133 let inbox = format!("agents.{name}.inbox");
134 let configured = self
135 .read_roster()
136 .map_err(|error| AgentError::Store(error.to_string()))?
137 .get(&inbox)
138 .cloned();
139 if let Some(configured) = configured {
140 self.quiesce_channel(&inbox, "agent_retired")
141 .map_err(|error| AgentError::Store(error.to_string()))?;
142 configured
143 .handle
144 .close()
145 .map_err(|_| AgentError::Delivery("retired inbox close failed"))?;
146 self.write_roster()
147 .map_err(|error| AgentError::Store(error.to_string()))?
148 .remove(&inbox);
149 }
150 self.console
151 .emit(crate::console::TopologyChange::Service {
152 subject: inbox,
153 present: false,
154 })
155 .map_err(|_| AgentError::Delivery("agent retirement topology failed"))?;
156 Ok(())
157 }
158 #[cfg(any(test, feature = "test-support"))]
163 pub fn with_agent_fixture(
164 mut self,
165 network: &str,
166 placer: std::sync::Arc<dyn crate::services::lifecycle::Placer>,
167 ) -> Result<Self, ServerError> {
168 self.agents = Some(crate::agents::runtime::Agents::open(
169 self.durable_store(),
170 crate::agents::config::AgentsConfig::default(),
171 network.into(),
172 self.cluster.supervisor().clone(),
173 self.console.device(),
174 Some(placer),
175 None,
176 )?);
177 self.agents
178 .as_ref()
179 .ok_or(AgentError::Invalid("agent fixture missing"))?
180 .bind_console(&self.console)?;
181 Ok(self)
182 }
183 #[cfg(any(test, feature = "test-support"))]
187 #[must_use]
188 pub fn with_console_tap_capacity(self, capacity: usize) -> Self {
189 self.console.set_tap_capacity(capacity);
190 self
191 }
192 pub(super) fn subscribe_agent(
193 &self,
194 connection: u64,
195 session: Option<&dyn LysSession>,
196 channel: &str,
197 schemas: &[ProtocolSchemaId],
198 install: Option<liminal::channel::InboxInstall>,
199 ) -> Result<ConnectionSubscription, ServerError> {
200 binding::check(session, crate::auth_lys::HotAction::Subscribe, channel)?;
201 if binding::subject(channel).is_none() {
202 let subscription = ConnectionServices::subscribe(self, channel, schemas, install)?;
203 return self.track_agent_listen(connection, channel, subscription);
204 }
205 let agents = self
206 .agents
207 .as_ref()
208 .ok_or(AgentError::Invalid(crate::agents::error::NO_REGISTRY))?;
209 self.place_own_station(session, channel)?;
210 self.ensure_agent_channel(channel)?;
211 let id = self
212 .next_subscription_id
213 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
214 let subscription = agents
215 .subscribe(
216 crate::agents::runtime::AgentSubscription {
217 services: self,
218 connection,
219 session,
220 subject: channel,
221 schemas,
222 install,
223 id,
224 },
225 |install| {
226 self.open_subscription(channel, schemas, install, None)
227 .map_err(|error| AgentError::Store(error.to_string()))
228 },
229 )
230 .map_err(ServerError::from)?;
231 self.track_agent_listen(connection, channel, subscription)
232 }
233
234 fn track_agent_listen(
235 &self,
236 connection: u64,
237 subject: &str,
238 subscription: ConnectionSubscription,
239 ) -> Result<ConnectionSubscription, ServerError> {
240 let Some(agents) = &self.agents else {
241 return Ok(subscription);
242 };
243 let ConnectionSubscription {
244 id,
245 channel,
246 stream_id,
247 selected_schema,
248 resource,
249 issued,
250 } = subscription;
251 let resource = agents.track_listen(connection, id, subject, resource)?;
252 Ok(ConnectionSubscription {
253 id,
254 channel,
255 stream_id,
256 selected_schema,
257 resource,
258 issued,
259 })
260 }
261
262 pub fn agent_created(&self, name: &str) -> Result<(), ServerError> {
268 self.ensure_agent_channel(&format!("agents.{name}.inbox"))?;
269 self.ensure_agent_channel(&format!("agents.{name}.cancel"))?;
270 self.ensure_agent_channel(&format!("agents.{name}.stream"))?;
271 if let Some(delivery) = &self.agent_delivery {
272 let inbox = self
273 .read_roster()
274 .map_err(|error| AgentError::Store(error.to_string()))?
275 .get(&format!("agents.{name}.inbox"))
276 .map(|channel| channel.handle.clone())
277 .ok_or(AgentError::Invalid("registered inbox absent"))?;
278 delivery.bind_inbox(name, inbox).map_err(draft_error)?;
279 }
280 self.agents
281 .as_ref()
282 .ok_or(AgentError::Invalid(crate::agents::error::NO_REGISTRY))?
283 .created(name)?;
284 self.console
285 .emit(crate::console::TopologyChange::Service {
286 subject: format!("agents.{name}.inbox"),
287 present: true,
288 })
289 .map_err(|_| AgentError::Delivery("agent topology change failed"))?;
290 Ok(())
291 }
292
293 pub(super) fn place_own_station(
298 &self,
299 session: Option<&dyn LysSession>,
300 channel: &str,
301 ) -> Result<(), ServerError> {
302 let (Some(session), Some((name, "inbox")), Some(agents)) =
303 (session, binding::subject(channel), self.agents.as_ref())
304 else {
305 return Ok(());
306 };
307 let owner = match session.holder_kind() {
308 Some("agent") if session.holder() == name => name.to_owned(),
309 Some(crate::agents::open_session::OPEN_KIND) if session.holder() == name => {
310 if name == crate::server::connection::declared_holder::ANONYMOUS {
311 return Err(AgentError::Invalid(crate::agents::error::ANONYMOUS_SEAT).into());
312 }
313 format!(
314 "{}{name}",
315 crate::server::connection::declared_holder::PREFIX
316 )
317 }
318 _ => return Ok(()),
319 };
320 if agents.registry().place_own(name, &owner, agents.now()?)? {
321 self.agent_created(name)?;
322 }
323 Ok(())
324 }
325
326 fn ensure_agent_channel(&self, channel: &str) -> Result<(), ServerError> {
327 let (name, leaf) = binding::subject(channel).ok_or(AgentError::Invalid("agent subject"))?;
328 let agents = self
329 .agents
330 .as_ref()
331 .ok_or(AgentError::Invalid(crate::agents::error::NO_REGISTRY))?;
332 let record = agents
333 .registry()
334 .record(name)
335 .map_err(|error| match error {
336 AgentError::Unknown(name) => AgentError::NoStation(name),
337 other => other,
338 })?;
339 if matches!(leaf, "presence" | "state") {
340 return Ok(());
341 }
342 if !record.active() {
343 return Err(AgentError::Transition("agent is retired").into());
344 }
345 if !matches!(leaf, "inbox" | "cancel" | "stream") {
346 return Ok(());
347 }
348 let schema = serde_json::to_vec(&serde_json::json!({"type":"object"}))
349 .map_err(|_| AgentError::Invalid("inbox schema encoding"))?;
350 self.register_channel(&ChannelRegistration {
351 name: channel.into(),
352 schema_bytes: (leaf == "inbox").then_some(schema),
353 durable: leaf == "inbox",
354 })
355 .map_err(|error| AgentError::Store(error.to_string()))?;
356 Ok(())
357 }
358
359 pub(super) fn agent_channel_admission(
360 &self,
361 operation: crate::server::connection::ChannelOperation,
362 channel: &str,
363 ) -> Option<Result<(), ChannelAccessError>> {
364 let (name, leaf) = binding::subject(channel)?;
365 if !matches!(leaf, "presence" | "state" | "inbox" | "cancel" | "stream") {
366 return None;
367 }
368 if operation == crate::server::connection::ChannelOperation::Subscribe
372 && leaf == "inbox"
373 && self
374 .agents
375 .as_ref()
376 .is_some_and(|agents| agents.registry().record(name).is_err())
377 {
378 return Some(Ok(()));
379 }
380 Some(self.ensure_agent_channel(channel).map_err(|error| {
381 ChannelAccessError::RosterUnavailable {
382 message: error.to_string(),
383 }
384 }))
385 }
386
387 pub fn agent_topology_payload<'a>(
392 &self,
393 subject: &str,
394 payload: &'a [u8],
395 ) -> Result<std::borrow::Cow<'a, [u8]>, crate::console::ConsoleError> {
396 let Some(agents) = &self.agents else {
397 return Ok(std::borrow::Cow::Borrowed(payload));
398 };
399 if subject != crate::console::TOPOLOGY_SUBJECT {
400 return Ok(std::borrow::Cow::Borrowed(payload));
401 }
402 let bytes = crate::console::topology::with_agents(payload, agents, self.console.device())
403 .map_err(|error| crate::console::ConsoleError::Server {
404 subject: subject.into(),
405 source: error.into(),
406 })?;
407 Ok(std::borrow::Cow::Owned(bytes))
408 }
409}
410
411fn validate_inbox(
412 bounds: &crate::agents::config::AgentsConfig,
413 payload: &[u8],
414) -> Result<(), AgentError> {
415 if payload.len() > bounds.max_event_bytes {
416 return Err(AgentError::Bound {
417 bound: "max_event_bytes",
418 limit: bounds.max_event_bytes,
419 count: payload.len(),
420 });
421 }
422 let value: serde_json::Value =
423 serde_json::from_slice(payload).map_err(|_| AgentError::Invalid("inbox JSON"))?;
424 let control = |value: &serde_json::Value, keys: &[&str]| {
427 value.as_object().is_some_and(|fields| {
428 fields.iter().all(|(key, value)| {
429 keys.contains(&key.as_str()) && (value.is_string() || value.is_boolean())
430 })
431 })
432 };
433 let accepted = value.as_object().is_some_and(|object| {
435 let named = object.get("from").map(serde_json::Value::is_string);
436 named != Some(false)
437 && object.len() == 1 + usize::from(named.is_some())
438 && (object.get("text").is_some_and(serde_json::Value::is_string)
439 || object
440 .get("config")
441 .is_some_and(|c| control(c, &["option", "value"]))
442 || object
443 .get("command")
444 .is_some_and(|c| control(c, &["name", "input"]))
445 || object
446 .get("session")
447 .is_some_and(|c| control(c, &["list", "new", "open"])))
448 });
449 if !accepted {
450 return Err(AgentError::Invalid(
451 "inbox accepts exactly one of {text}, {command}, {config} or {session}",
452 ));
453 }
454 Ok(())
455}
456
457fn check_sender(payload: &[u8], holder: Option<&str>) -> Result<(), AgentError> {
462 let value: serde_json::Value =
463 serde_json::from_slice(payload).map_err(|_| AgentError::Invalid("inbox JSON"))?;
464 match (
465 value.get("from").and_then(serde_json::Value::as_str),
466 holder,
467 ) {
468 (None, _) => Ok(()),
469 (Some(claimed), Some(holder)) if claimed == holder => Ok(()),
470 _ => Err(AgentError::Invalid(
471 "inbox from names a sender other than the session's holder",
472 )),
473 }
474}
475
476#[cfg(test)]
477#[path = "server_tests.rs"]
478mod tests;
479
480type AgentOwners = (
481 Option<std::sync::Arc<crate::agents::runtime::Agents>>,
482 Option<std::sync::Arc<crate::agents::waiting::durable::ApprovalAgents>>,
483 Option<std::sync::Arc<crate::agents::waiting::AgentDelivery>>,
484);
485fn draft_error(error: crate::auth_lys::drafts::DraftError) -> ServerError {
486 error.into()
487}
488impl From<crate::auth_lys::drafts::DraftError> for ServerError {
489 fn from(error: crate::auth_lys::drafts::DraftError) -> Self {
490 Self::ConfigValidation {
491 message: error.to_string(),
492 }
493 }
494}