pub mod actions;
use crate::llm::action_helper::call_llm;
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::IDENT_QUERY_EVENT;
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
pub const MAX_QUERY_BYTES: usize = 1024;
const QUERY_READ_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"";
pub struct IdentServer;
impl IdentServer {
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,
) -> Result<SocketAddr> {
let listener = TcpListener::bind(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!("IDENT server listening on {}", local_addr));
let protocol = Arc::new(actions::IdentProtocol::new());
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,
"IDENT",
Some(&status_tx),
)
.await
{
Ok((socket, peer_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
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: peer_addr,
local_addr,
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(),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
Log::new(Some(&status_tx))
.info(format!("IDENT client connected from {}", peer_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 registrar = app_state.clone();
let conn_handle = tokio::spawn(async move {
let _permit = permit;
handle_ident_connection(
socket,
peer_addr,
llm_clone,
state_clone,
status_clone,
server_id,
protocol_clone,
connection_id,
)
.await
});
registrar.register_server_task(server_id, conn_handle).await;
}
Err(e) => {
Log::new(Some(&status_tx)).error(format!("IDENT accept error: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
#[derive(Debug, PartialEq, Eq)]
enum ParsedQuery {
Valid { server_port: u16, client_port: u16 },
Invalid { echo: String },
}
fn echo_field(raw: &str) -> String {
let without_separators: String = raw.chars().filter(|c| *c != ':' && *c != ',').collect();
crate::utils::sanitize::token(&without_separators, 16)
}
fn parse_query(line: &str) -> ParsedQuery {
let trimmed = line.trim();
let mut halves = trimmed.splitn(2, ',');
let left = halves.next().unwrap_or("").trim();
let Some(right) = halves.next().map(str::trim) else {
return ParsedQuery::Invalid {
echo: echo_field(trimmed),
};
};
match (left.parse::<i64>(), right.parse::<i64>()) {
(Ok(sp), Ok(cp)) if (1..=65535).contains(&sp) && (1..=65535).contains(&cp) => {
ParsedQuery::Valid {
server_port: sp as u16,
client_port: cp as u16,
}
}
_ => ParsedQuery::Invalid {
echo: format!("{} , {}", echo_field(left), echo_field(right)),
},
}
}
fn enforce_port_pair(reply: &[u8], server_port: u16, client_port: u16) -> Option<Vec<u8>> {
let text = std::str::from_utf8(reply).ok()?;
let colon = text.find(':')?;
let (pair, rest) = text.split_at(colon);
let mut halves = pair.splitn(2, ',');
let left = halves.next()?.trim().parse::<u16>().ok()?;
let right = halves.next()?.trim().parse::<u16>().ok()?;
if left == server_port && right == client_port {
return None;
}
Some(format!("{} , {} {}", server_port, client_port, rest).into_bytes())
}
fn classify_decision(raw_actions: &[serde_json::Value]) -> Option<&'static str> {
let mut saw_error = false;
for action in raw_actions {
match action.get("type").and_then(|v| v.as_str()) {
Some("send_ident_userid") => return Some("model_userid"),
Some("send_ident_error") => saw_error = true,
_ => {}
}
}
saw_error.then_some("model_reject")
}
#[allow(clippy::too_many_arguments)]
async fn handle_ident_connection(
socket: tokio::net::TcpStream,
peer_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
protocol: Arc<actions::IdentProtocol>,
connection_id: ConnectionId,
) {
let (reader, write_half) = tokio::io::split(socket);
let write_half = Arc::new(Mutex::new(write_half));
let peer_rx = crate::server::peer_support::register_peer_channel(
&app_state,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol.clone(),
app_state.clone(),
server_id,
connection_id.as_u32(),
write_half.clone(),
status_tx.clone(),
);
run_ident_session(
reader,
&write_half,
peer_addr,
&llm_client,
&app_state,
&status_tx,
server_id,
&protocol,
connection_id,
)
.await;
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
let _ = write_half.lock().await.shutdown().await;
use crate::state::server::ConnectionStatus;
app_state
.update_connection_status(server_id, connection_id, ConnectionStatus::Closed)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
}
async fn write_counted<W>(
write_half: &Arc<Mutex<W>>,
data: &[u8],
app_state: &AppState,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
) -> std::io::Result<()>
where
W: tokio::io::AsyncWrite + Unpin,
{
{
let mut writer = write_half.lock().await;
writer.write_all(data).await?;
writer.flush().await?;
}
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(data.len() as u64),
None,
Some(1),
)
.await;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn run_ident_session<R, W>(
mut reader: R,
write_half: &Arc<Mutex<W>>,
peer_addr: SocketAddr,
llm_client: &OllamaClient,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
protocol: &Arc<actions::IdentProtocol>,
connection_id: ConnectionId,
) where
R: tokio::io::AsyncRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
let log = Log::new(Some(status_tx));
let Some(line) = read_query_line(
&mut reader,
peer_addr,
app_state,
server_id,
connection_id,
&log,
)
.await
else {
return;
};
log.trace(format!("IDENT query line from {}: {:?}", peer_addr, line));
let (server_port, client_port) = match parse_query(&line) {
ParsedQuery::Invalid { echo } => {
log.warn(format!(
"IDENT query from {} decision=invalid_port (rejected in-process, no LLM call)",
peer_addr
));
let reply = actions::format_error_reply_raw_pair(&echo, "INVALID-PORT");
let _ = write_counted(write_half, &reply, app_state, server_id, connection_id).await;
return;
}
ParsedQuery::Valid {
server_port,
client_port,
} => (server_port, client_port),
};
let event = Event::new(
&IDENT_QUERY_EVENT,
serde_json::json!({
"server_port": server_port,
"client_port": client_port,
"source_addr": peer_addr.to_string(),
}),
);
log.debug(format!(
"IDENT calling LLM for {},{} from {}",
server_port, client_port, peer_addr
));
let outcome = call_llm(
llm_client,
app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await;
let (decision, reply) = match outcome {
Ok(execution_result) => {
for message in &execution_result.messages {
log.info(message);
}
let mut output: Option<Vec<u8>> = None;
for protocol_result in &execution_result.protocol_results {
if let crate::llm::actions::protocol_trait::ActionResult::Output(bytes) =
protocol_result
{
output.get_or_insert_with(Vec::new).extend_from_slice(bytes);
}
}
match output {
Some(bytes) => {
let decision =
classify_decision(&execution_result.raw_actions).unwrap_or("model_answer");
let bytes = match enforce_port_pair(&bytes, server_port, client_port) {
Some(corrected) => {
log.warn(format!(
"IDENT {},{} from {}: the answer carried a different port \
pair; rewritten to the one queried, which is what the \
client matches on",
server_port, client_port, peer_addr
));
corrected
}
None => bytes,
};
(decision, bytes)
}
None => {
log.warn(format!(
"IDENT {},{} from {}: {} action(s) failed and nothing reached the wire",
server_port,
client_port,
peer_addr,
execution_result.failures.len()
));
(
"model_silent",
actions::format_error_reply(server_port, client_port, "UNKNOWN-ERROR"),
)
}
}
}
Err(e) => {
let category = if crate::utils::WireFailure::classify(&e).is_overloaded() {
"overloaded"
} else {
"unavailable"
};
log.error(format!(
"IDENT {},{} from {} decision=fail_closed_llm_error category={} (answering \
UNKNOWN-ERROR because no decision was produced)",
server_port, client_port, peer_addr, category
));
log.debug(format!("IDENT LLM call failed: {}", e));
(
"fail_closed_llm_error",
actions::format_error_reply(server_port, client_port, "UNKNOWN-ERROR"),
)
}
};
let summary = format!(
"IDENT {},{} from {} decision={}",
server_port, client_port, peer_addr, decision
);
if decision == "fail_closed_llm_error" || decision == "model_silent" {
log.debug(&summary);
} else {
log.info(&summary);
}
log.trace(format!(
"IDENT reply to {}: {}",
peer_addr,
String::from_utf8_lossy(&reply).trim_end()
));
if let Err(e) = write_counted(write_half, &reply, app_state, server_id, connection_id).await {
log.error(format!("IDENT write error to {}: {}", peer_addr, e));
}
}
async fn read_query_line<R>(
reader: &mut R,
peer_addr: SocketAddr,
app_state: &Arc<AppState>,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
log: &Log<'_>,
) -> Option<String>
where
R: tokio::io::AsyncRead + Unpin,
{
let mut buffer = vec![0u8; 512];
let mut accumulated: Vec<u8> = Vec::new();
loop {
let read = match tokio::time::timeout(QUERY_READ_TIMEOUT, reader.read(&mut buffer)).await {
Err(_) => {
log.info(format!(
"IDENT client {} sent no query within {}s; closing",
peer_addr,
QUERY_READ_TIMEOUT.as_secs()
));
return None;
}
Ok(result) => result,
};
match read {
Ok(0) => {
if accumulated.is_empty() {
log.info(format!(
"IDENT client {} disconnected without sending a query",
peer_addr
));
return None;
}
break;
}
Ok(n) => {
app_state
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
log.debug(format!("IDENT received {} bytes from {}", n, peer_addr));
accumulated.extend_from_slice(&buffer[..n]);
if accumulated.contains(&b'\n') {
break;
}
if accumulated.len() > MAX_QUERY_BYTES {
log.warn(format!(
"IDENT query from {} exceeded {} bytes with no newline; \
answering INVALID-PORT",
peer_addr, MAX_QUERY_BYTES
));
break;
}
}
Err(e) => {
log.error(format!("IDENT read error from {}: {}", peer_addr, e));
return None;
}
}
}
let text = String::from_utf8_lossy(&accumulated).to_string();
Some(text.lines().next().unwrap_or("").to_string())
}