liminal-server 0.24.0

Standalone server for the liminal messaging bus
Documentation
//! Closed host services bind every request to its admitted agent identity.

use serde::Deserialize;
use serde_json::{Value, json};

use super::{LiminalConnectionServices, ServerError};
use crate::agents::{
    binding,
    error::AgentError,
    records::{AgentEvent, AgentTransition},
};
use crate::auth_lys::{HotAction, session::LysSession};

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct HostBinding {
    agent: String,
    session: String,
    protocol_version: u8,
    load_session: bool,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Close {
    agent: String,
    present: bool,
    exit_status: i32,
    session_state: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Refuse {
    agent: String,
    reason: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Permission {
    agent: String,
    operation: String,
    act: String,
    target: Option<String>,
    arguments: Option<Value>,
    words: String,
}

impl LiminalConnectionServices {
    pub(super) fn agent_host_request(
        &self,
        session: Option<&dyn LysSession>,
        subject: &str,
        bytes: &[u8],
        at_ms: u64,
    ) -> Option<Result<Value, ServerError>> {
        if !binding::is_host_route(subject) {
            return None;
        }
        let result = (|| -> Result<Value, AgentError> {
            let agents = self
                .agents
                .as_ref()
                .ok_or(AgentError::Invalid("agent registry absent"))?;
            if bytes.len() > agents.bounds().max_event_bytes {
                return Err(AgentError::Invalid("host request bound"));
            }
            let value: Value = serde_json::from_slice(bytes)
                .map_err(|_| AgentError::Invalid("host request JSON"))?;
            let name = value["agent"]
                .as_str()
                .ok_or(AgentError::Invalid("host agent claim"))?
                .to_owned();
            binding::claim(session, &name)?;
            binding::decide(
                session,
                agents.network(),
                HotAction::Subscribe,
                &format!("agents.{name}.inbox"),
                at_ms,
            )?;
            let record = agents.registry().record(&name)?;
            if !record.active() {
                return Err(AgentError::Transition("agent is retired"));
            }
            let event = match subject {
                "agents.session" => session_event(&record, agents.bounds(), parse(value)?)?,
                "agents.session_close" => close_event(agents, &record, parse(value)?)?,
                "agents.refuse_prompt" => {
                    let body: Refuse = parse(value)?;
                    agents.bounds().field(&body.agent, "host agent")?;
                    agents.bounds().field(&body.reason, "prompt refusal")?;
                    self.host_notice(
                        &body.agent,
                        &json!({"type":"agent.host_refusal","reason":body.reason}),
                    )?;
                    return Ok(json!({"refused":true}));
                }
                "agents.permission" => {
                    let body: Permission = parse(value)?;
                    agents.bounds().field(&body.agent, "host agent")?;
                    if body.act != "agent_permission"
                        || body.target.is_some()
                        || body.arguments.is_some()
                    {
                        return Err(AgentError::Invalid("prepared host act contract"));
                    }
                    let (config, handle) = self
                        .host_config
                        .get()
                        .ok_or(AgentError::Invalid("host approval config absent"))?;
                    let owners = self
                        .approval_agents
                        .as_ref()
                        .ok_or(AgentError::Invalid("host approval owner absent"))?;
                    let lifecycle = self
                        .site_lifecycle
                        .get()
                        .ok_or(AgentError::Invalid("host target owner absent"))?;
                    return session
                        .ok_or(AgentError::Invalid("host session missing"))?
                        .hold_permission(crate::auth_lys::prepared::PermissionRequest {
                            lifecycle,
                            owners,
                            config,
                            handle,
                            operation: &body.operation,
                            words: &body.words,
                            at_ms,
                        })
                        .map_err(|error| AgentError::Store(error.name().into()));
                }
                _ => return Err(AgentError::Invalid("host route")),
            };
            agents.registry().observe(
                &name,
                AgentTransition {
                    holder: name.clone(),
                    at_ms,
                    change: event,
                },
            )?;
            Ok(json!({"recorded":true}))
        })();
        Some(result.map_err(ServerError::from))
    }

    fn host_notice(&self, name: &str, value: &Value) -> Result<(), AgentError> {
        self.agent_created(name)
            .map_err(|error| AgentError::Store(error.to_string()))?;
        let inbox = self
            .read_roster()
            .map_err(|error| AgentError::Store(error.to_string()))?
            .get(&format!("agents.{name}.inbox"))
            .map(|channel| channel.handle.clone())
            .ok_or(AgentError::Invalid("host inbox absent"))?;
        let bytes =
            serde_json::to_vec(value).map_err(|_| AgentError::Invalid("host notice encoding"))?;
        let delivery = inbox
            .publish_with_delivery(
                bytes,
                liminal::envelope::PublisherId::new(format!("liminal.{}", self.console.device())),
                None,
            )
            .map_err(|_| AgentError::Delivery("host notice publication failed"))?;
        if !delivery.is_admitted() {
            return Err(AgentError::Delivery("host notice refused"));
        }
        Ok(())
    }
}

fn parse<T: serde::de::DeserializeOwned>(value: Value) -> Result<T, AgentError> {
    serde_json::from_value(value).map_err(|_| AgentError::Invalid("host request fields"))
}

fn session_event(
    record: &crate::agents::records::AgentRecord,
    bounds: &crate::agents::config::AgentsConfig,
    body: HostBinding,
) -> Result<AgentEvent, AgentError> {
    bounds.field(&body.agent, "host agent")?;
    bounds.field(&body.session, "ACP session")?;
    if !matches!(body.protocol_version, 1 | 2) {
        return Err(AgentError::Invalid("ACP protocol version"));
    }
    let previous =
        record
            .transitions
            .iter()
            .rev()
            .find_map(|transition| match &transition.change {
                AgentEvent::Session { session, .. } => Some(session),
                _ => None,
            });
    let ended = record
        .transitions
        .iter()
        .rev()
        .find_map(|transition| match &transition.change {
            AgentEvent::SessionClosed { session_state, .. } => Some(session_state == "ended"),
            AgentEvent::Session { .. } => Some(false),
            _ => None,
        })
        .unwrap_or(false);
    if ended && previous == Some(&body.session) {
        return Err(AgentError::Transition("ended session cannot resume"));
    }
    Ok(AgentEvent::Session {
        session: body.session,
        protocol_version: body.protocol_version,
        load_session: body.load_session,
    })
}

fn close_event(
    agents: &crate::agents::runtime::Agents,
    record: &crate::agents::records::AgentRecord,
    body: Close,
) -> Result<AgentEvent, AgentError> {
    agents.bounds().field(&body.agent, "host agent")?;
    if body.present || !matches!(body.session_state.as_str(), "dormant" | "ended") {
        return Err(AgentError::Invalid("host ending"));
    }
    let resumable = record
        .transitions
        .iter()
        .rev()
        .find_map(|transition| match &transition.change {
            AgentEvent::Session { load_session, .. } => Some(*load_session),
            _ => None,
        })
        .ok_or(AgentError::Transition("host session absent"))?;
    if body.session_state == "dormant" && !resumable {
        return Err(AgentError::Transition("host session cannot resume"));
    }
    if let Some(connection) = record.connection() {
        agents.close(connection, "harness_exit")?;
    }
    Ok(AgentEvent::SessionClosed {
        exit_status: body.exit_status,
        session_state: body.session_state,
    })
}