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,
})
}