pub mod actions;
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use actions::{XmppProtocol, XMPP_DATA_RECEIVED_EVENT};
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::mpsc;
pub const MAX_XMPP_BUFFER_BYTES: usize = 256 * 1024;
const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_STANZAS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"<?xml version='1.0'?>\
<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' \
version='1.0'><stream:error>\
<policy-violation xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>\
<text xmlns='urn:ietf:params:xml:ns:xmpp-streams' xml:lang='en'>\
connection limit reached</text></stream:error></stream:stream>";
fn stream_error_frame(
failure: crate::utils::WireFailure,
stream_opened: bool,
domain: &str,
) -> String {
const NS: &str = "urn:ietf:params:xml:ns:xmpp-streams";
let condition = match failure {
crate::utils::WireFailure::Overloaded => "resource-constraint",
crate::utils::WireFailure::Unavailable => "internal-server-error",
};
let mut frame = String::new();
if !stream_opened {
frame.push_str(&format!(
"<?xml version='1.0'?><stream:stream xmlns='jabber:client' \
xmlns:stream='http://etherx.jabber.org/streams' version='1.0' from='{}'>",
actions::xml_escape(domain)
));
}
frame.push_str(&format!(
"<stream:error><{condition} xmlns='{NS}'/>\
<text xmlns='{NS}' xml:lang='en'>{}</text></stream:error></stream:stream>",
actions::xml_escape(failure.text())
));
frame
}
pub struct XmppServer;
impl XmppServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
domain: String,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> Result<SocketAddr> {
let first_byte_timeout = first_byte_timeout_secs
.map(std::time::Duration::from_secs)
.unwrap_or(FIRST_BYTE_READ_TIMEOUT);
let idle_timeout = idle_timeout_secs
.map(std::time::Duration::from_secs)
.unwrap_or(IDLE_BETWEEN_STANZAS_TIMEOUT);
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"XMPP server (action-based) listening on {} for domain {}",
local_addr, domain
));
let stream_domain = domain.clone();
let protocol = Arc::new(XmppProtocol::with_domain(domain));
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"XMPP",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
let llm_clone = llm_client.clone();
let state_clone = app_state.clone();
let status_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let domain_clone = stream_domain.clone();
Log::new(Some(&status_clone)).debug(format!(
"XMPP connection {} from {}",
connection_id, remote_addr
));
let task_owner = app_state.clone();
task_owner.spawn_server_task(server_id, async move {
let _permit = permit;
let (read_half, write_half) = tokio::io::split(stream);
let write_half_arc = Arc::new(tokio::sync::Mutex::new(write_half));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
state_clone
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_clone.send("__UPDATE_UI__".to_string());
let peer_rx = crate::server::peer_support::register_peer_channel(
&state_clone,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol_clone.clone(),
state_clone.clone(),
server_id,
connection_id.as_u32(),
write_half_arc.clone(),
status_clone.clone(),
);
let mut read_half = read_half;
let mut buffer = Vec::new();
let mut temp_buf = vec![0u8; 4096];
let mut stream_opened = false;
loop {
let read_timeout = if stream_opened {
idle_timeout
} else {
first_byte_timeout
};
let read_result = match tokio::time::timeout(
read_timeout,
read_half.read(&mut temp_buf),
)
.await
{
Ok(result) => result,
Err(_) => {
Log::new(Some(&status_clone)).info(format!(
"XMPP connection {} sent nothing for {}s; closing \
idle connection \
decision=fail_closed_read_timeout",
connection_id,
read_timeout.as_secs()
));
break;
}
};
match read_result {
Ok(0) => {
Log::new(Some(&status_clone)).debug(format!(
"XMPP connection {} closed by client",
connection_id
));
break;
}
Ok(n) => {
buffer.extend_from_slice(&temp_buf[..n]);
state_clone
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
if buffer.len() > MAX_XMPP_BUFFER_BYTES {
Log::new(Some(&status_clone)).error(format!(
"XMPP connection {} buffered {} bytes without \
completing a stanza (limit {}), closing",
connection_id,
buffer.len(),
MAX_XMPP_BUFFER_BYTES
));
break;
}
let log = Log::new(Some(&status_clone));
log.debug(format!(
"XMPP received {} bytes on connection {}",
n, connection_id
));
log.trace(format!(
"XMPP data (XML): {}",
String::from_utf8_lossy(&buffer)
));
let xml_data = String::from_utf8_lossy(&buffer).to_string();
let event = Event::new(
&XMPP_DATA_RECEIVED_EVENT,
serde_json::json!({
"xml_data": xml_data
}),
);
log.debug(format!(
"XMPP calling LLM for connection {}",
connection_id
));
match call_llm(
&llm_clone,
&state_clone,
server_id,
Some(connection_id),
&event,
protocol_clone.as_ref(),
)
.await
{
Ok(execution_result) => {
for message in &execution_result.messages {
log.info(format!("{}", message));
}
log.debug(format!(
"XMPP got {} protocol results",
execution_result.protocol_results.len()
));
let mut should_close = false;
let mut wait_for_more = false;
let mut wrote_any = false;
for protocol_result in
execution_result.protocol_results
{
match protocol_result {
ActionResult::Output(data) => {
let xml_str =
String::from_utf8_lossy(&data);
let mut write =
write_half_arc.lock().await;
let _ = write.write_all(&data).await;
let _ = write.flush().await;
drop(write);
stream_opened = true;
wrote_any = true;
state_clone
.update_connection_stats(
server_id,
connection_id,
None,
Some(data.len() as u64),
None,
Some(1),
)
.await;
log.debug(format!("XMPP sent {} bytes on connection {}", data.len(), connection_id));
log.trace(format!(
"XMPP sent (XML): {}",
xml_str
));
}
ActionResult::CloseConnection => {
should_close = true;
}
ActionResult::WaitForMore => {
wait_for_more = true;
log.debug("XMPP waiting for more data");
}
_ => {}
}
}
if should_close {
log.debug(format!(
"XMPP connection {connection_id} closed \
by model: decision=model_close"
));
break;
}
if !wrote_any && !wait_for_more {
log.debug(format!(
"XMPP no stanza for connection \
{connection_id}: \
decision=model_no_actions"
));
}
if !wait_for_more {
buffer.clear();
}
}
Err(e) => {
let failure =
crate::utils::WireFailure::classify(&e);
let class = if failure.is_overloaded() {
"overloaded"
} else {
"unavailable"
};
log.warn(format!(
"XMPP LLM call failed on connection \
{connection_id}: \
decision=fail_closed_llm_error \
class={class} error={e}"
));
let frame = stream_error_frame(
failure,
stream_opened,
&domain_clone,
);
let mut write = write_half_arc.lock().await;
let _ = write.write_all(frame.as_bytes()).await;
let _ = write.flush().await;
drop(write);
state_clone
.update_connection_stats(
server_id,
connection_id,
None,
Some(frame.len() as u64),
None,
Some(1),
)
.await;
break;
}
}
}
Err(e) => {
Log::new(Some(&status_clone)).error(format!(
"XMPP read error on connection {}: {}",
connection_id, e
));
break;
}
}
}
state_clone
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
state_clone
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_clone.send("__UPDATE_UI__".to_string());
}).await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to accept XMPP connection: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}