pub mod actions;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use serde_json::json;
use std::collections::HashMap;
use tokio::sync::mpsc;
use tokio::sync::RwLock;
use tracing::{debug, error};
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::server::openapi::actions::OpenApiProtocol;
use crate::state::app_state::AppState;
use matchit::Router;
use openapi_rs::model::parse::OpenAPI;
#[derive(Clone, Debug)]
pub struct RouteMetadata {
pub operation_id: Option<String>,
pub method: String,
pub path_template: String,
pub operation_json: serde_json::Value, }
#[derive(Debug)]
pub enum MatchResult {
Found {
metadata: RouteMetadata,
params: HashMap<String, String>,
},
MethodNotAllowed { allowed_methods: Vec<String> },
NotFound,
}
pub struct OpenApiState {
pub spec: Option<String>,
pub spec_valid: bool,
pub parsed_spec: Option<OpenAPI>,
pub router: Option<Router<RouteMetadata>>,
pub llm_on_invalid: bool,
}
impl OpenApiState {
pub fn new() -> Self {
Self {
spec: None,
spec_valid: false,
parsed_spec: None,
router: None,
llm_on_invalid: false, }
}
}
fn build_router(spec: &OpenAPI) -> anyhow::Result<Router<RouteMetadata>> {
let mut router = Router::new();
for (path_template, path_item) in &spec.paths {
for (method_lower, operation) in &path_item.operations {
let method = method_lower.to_uppercase();
let operation_json = serde_json::to_value(operation)?;
let metadata = RouteMetadata {
operation_id: operation.operation_id.clone(),
method: method.clone(),
path_template: path_template.clone(),
operation_json,
};
let route_key = format!("{}:{}", method, path_template);
router.insert(&route_key, metadata)?;
debug!("Registered OpenAPI route: {} {}", method, path_template);
}
}
Ok(router)
}
fn match_route(router: &Router<RouteMetadata>, method: &str, path: &str) -> MatchResult {
let route_key = format!("{}:{}", method, path);
match router.at(&route_key) {
Ok(matched) => {
let params: HashMap<String, String> = matched
.params
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
MatchResult::Found {
metadata: matched.value.clone(),
params,
}
}
Err(_) => {
let allowed_methods = find_allowed_methods(router, path);
if !allowed_methods.is_empty() {
MatchResult::MethodNotAllowed { allowed_methods }
} else {
MatchResult::NotFound
}
}
}
}
fn find_allowed_methods(router: &Router<RouteMetadata>, path: &str) -> Vec<String> {
let methods = [
"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE",
];
let mut allowed = Vec::new();
for method in &methods {
let route_key = format!("{}:{}", method, path);
if router.at(&route_key).is_ok() {
allowed.push(method.to_string());
}
}
allowed
}
fn validate_request(
_operation_json: &serde_json::Value,
_method: &str,
_path: &str,
_headers: &HashMap<String, String>,
_body: &str,
) -> anyhow::Result<()> {
Ok(())
}
pub const MAX_REQUEST_BODY_BYTES: usize = 8 * 1024 * 1024;
const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(75);
const MAX_CONNECTIONS: usize = 128;
const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
fn payload_too_large() -> Response<Full<Bytes>> {
Response::builder()
.status(413)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(
json!({
"error": "Payload Too Large",
"message": "The request body exceeds this server's limit"
})
.to_string(),
)))
.unwrap_or_else(|_| Response::new(Full::new(Bytes::new())))
}
fn immediate_404() -> Response<Full<Bytes>> {
Response::builder()
.status(404)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(
json!({
"error": "Not Found",
"message": "The requested path does not exist in the OpenAPI specification"
})
.to_string(),
)))
.unwrap()
}
fn immediate_405(allowed_methods: Vec<String>) -> Response<Full<Bytes>> {
let allow_header = allowed_methods.join(", ");
Response::builder()
.status(405)
.header("Content-Type", "application/json")
.header("Allow", allow_header.clone())
.body(Full::new(Bytes::from(
json!({
"error": "Method Not Allowed",
"message": format!("This path does not support the requested method"),
"allowed_methods": allowed_methods
})
.to_string(),
)))
.unwrap()
}
fn immediate_400(error_message: String) -> Response<Full<Bytes>> {
Response::builder()
.status(400)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(
json!({
"error": "Bad Request",
"message": error_message
})
.to_string(),
)))
.unwrap()
}
fn failure_response(failure: crate::utils::WireFailure) -> Response<Full<Bytes>> {
let (status, reason) = if failure.is_overloaded() {
(503, "Service Unavailable")
} else {
(500, "Internal Server Error")
};
let body = json!({
"error": reason,
"message": failure.text(),
})
.to_string();
let mut builder = Response::builder()
.status(status)
.header("Content-Type", "application/json");
if failure.is_overloaded() {
builder = builder.header(hyper::header::RETRY_AFTER, "1");
}
builder
.body(Full::new(Bytes::from(body)))
.unwrap_or_else(|_| Response::new(Full::new(Bytes::new())))
}
fn llm_error_response(
error: anyhow::Error,
method: &str,
path: &str,
status_tx: &mpsc::UnboundedSender<String>,
) -> Response<Full<Bytes>> {
let failure = crate::utils::WireFailure::classify(&error);
let category = if failure.is_overloaded() {
"overloaded"
} else {
"unavailable"
};
Log::new(Some(status_tx)).error(format!(
"OpenAPI {} {} decision=fail_closed_llm_error category={}: {}",
method, path, category, error
));
failure_response(failure)
}
async fn handle_llm_response(
execution_result: crate::llm::actions::executor::ExecutionResult,
status_tx: mpsc::UnboundedSender<String>,
openapi_state: Arc<RwLock<OpenApiState>>,
method: String,
path: String,
) -> Result<Response<Full<Bytes>>, Infallible> {
debug!("LLM OpenAPI response received");
for msg in execution_result.messages {
let _ = status_tx.send(msg);
}
let mut status_code = 200;
let mut response_headers = HashMap::new();
let mut response_body = String::new();
let mut spec_to_load: Option<String> = None;
let mut produced_response = false;
let mut decision = "fail_closed_no_action";
for protocol_result in execution_result.protocol_results {
match protocol_result {
ActionResult::Custom { name, data } => {
match name.as_str() {
"send_openapi_response" => {
produced_response = true;
decision = "model_response";
if let Some(status) = data.get("status_code").and_then(|v| v.as_u64()) {
status_code = status as u16;
}
if let Some(headers_obj) = data.get("headers").and_then(|v| v.as_object()) {
for (k, v) in headers_obj {
if let Some(v_str) = v.as_str() {
response_headers.insert(k.clone(), v_str.to_string());
}
}
}
if let Some(body) = data.get("body").and_then(|v| v.as_str()) {
response_body = body.to_string();
}
}
"send_validation_error" => {
produced_response = true;
decision = "model_reject";
status_code = 400;
if let Some(status) = data.get("status_code").and_then(|v| v.as_u64()) {
status_code = status as u16;
}
if let Some(message) = data.get("message").and_then(|v| v.as_str()) {
response_body = json!({
"error": message
})
.to_string();
response_headers
.insert("content-type".to_string(), "application/json".to_string());
}
}
"load_openapi_spec" | "reload_spec" => {
if let Some(spec) = data.get("spec").and_then(|v| v.as_str()) {
spec_to_load = Some(spec.to_string());
}
}
"configure_error_handling" => {
if let Some(llm_on_invalid) =
data.get("llm_on_invalid").and_then(|v| v.as_bool())
{
let mut state = openapi_state.write().await;
state.llm_on_invalid = llm_on_invalid;
Log::new(Some(&status_tx))
.info(format!("OpenAPI llm_on_invalid set to: {}", llm_on_invalid));
}
}
_ => {
debug!("Unknown custom action: {}", name);
}
}
}
ActionResult::Output(output_data) => {
if let Ok(json_value) = serde_json::from_slice::<serde_json::Value>(&output_data) {
produced_response = true;
decision = "model_output_legacy";
status_code = 500;
match json_value.get("status").and_then(|v| v.as_u64()) {
Some(status) => match u16::try_from(status)
.ok()
.filter(|code| (100..=599).contains(code))
{
Some(code) => status_code = code,
None => error!(
"OpenAPI legacy output carried status {} which is not an HTTP \
status; answering 500",
status
),
},
None => status_code = 200,
}
if let Some(headers_obj) = json_value.get("headers").and_then(|v| v.as_object())
{
for (k, v) in headers_obj {
if let Some(v_str) = v.as_str() {
response_headers.insert(k.clone(), v_str.to_string());
}
}
}
if let Some(body) = json_value.get("body").and_then(|v| v.as_str()) {
response_body = body.to_string();
}
}
}
_ => {}
}
}
if let Some(spec) = spec_to_load {
let mut state = openapi_state.write().await;
match openapi_rs::model::parse::OpenAPI::yaml(&spec) {
Ok(parsed_spec) => {
Log::new(Some(&status_tx)).info(format!(
"OpenAPI spec loaded successfully: {} bytes",
spec.len()
));
match build_router(&parsed_spec) {
Ok(router) => {
let route_count = parsed_spec.paths.len();
Log::new(Some(&status_tx))
.info(format!("Built OpenAPI router with {} routes", route_count));
state.spec = Some(spec);
state.spec_valid = true;
state.parsed_spec = Some(parsed_spec);
state.router = Some(router);
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to build OpenAPI router: {}", e));
state.spec = Some(spec);
state.spec_valid = false;
}
}
}
Err(e) => {
Log::new(Some(&status_tx)).error(format!("Failed to parse OpenAPI spec: {}", e));
state.spec = Some(spec);
state.spec_valid = false;
}
}
}
if !produced_response {
Log::new(Some(&status_tx)).warn(format!(
"OpenAPI {} {} decision=fail_closed_no_action -> 500 (no response action returned)",
method, path
));
return Ok(failure_response(crate::utils::WireFailure::Unavailable));
}
Log::new(Some(&status_tx)).info(format!(
"OpenAPI {} {} -> {} ({} bytes, decision={})",
method,
path,
status_code,
response_body.len(),
decision
));
Ok(crate::server::http_common::handler::build_safe_response(
status_code,
response_headers,
response_body,
"OpenAPI",
))
}
pub struct OpenApiServer;
impl OpenApiServer {
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,
startup_params: Option<crate::protocol::StartupParams>,
) -> anyhow::Result<SocketAddr> {
let openapi_state = Arc::new(RwLock::new(OpenApiState::new()));
let protocol = Arc::new(OpenApiProtocol::new());
let spec_loaded = if let Some(ref params) = startup_params {
let spec_content = if let Some(spec_str) = params.get_optional_string("spec")? {
Log::new(Some(&status_tx)).info(format!(
"OpenAPI spec provided via startup_params ({} bytes)",
spec_str.len()
));
Some(spec_str)
} else {
let msg = "OpenAPI server requires 'spec' parameter in startup_params. LLM must read the spec file and provide content.";
Log::new(Some(&status_tx)).error(msg);
return Err(anyhow::anyhow!(msg));
};
if let Some(spec) = spec_content {
let mut state = openapi_state.write().await;
state.spec = Some(spec.clone());
match openapi_rs::model::parse::OpenAPI::yaml(&spec) {
Ok(parsed) => match build_router(&parsed) {
Ok(router) => {
let route_count = parsed.paths.len();
Log::new(Some(&status_tx)).info(format!(
"Successfully built OpenAPI router with {} routes",
route_count
));
state.parsed_spec = Some(parsed);
state.router = Some(router);
state.spec_valid = true;
true
}
Err(e) => {
let msg = format!("Failed to build router: {}", e);
Log::new(Some(&status_tx)).error(&msg);
state.spec_valid = false;
return Err(anyhow::anyhow!(msg));
}
},
Err(e) => {
let msg = format!("Failed to parse OpenAPI spec: {}", e);
Log::new(Some(&status_tx)).error(&msg);
state.spec_valid = false;
return Err(anyhow::anyhow!(msg));
}
}
} else {
false
}
} else {
false
};
if !spec_loaded {
Log::new(Some(&status_tx))
.info("Starting OpenAPI server (spec will be provided by LLM on first request)...");
} else {
Log::new(Some(&status_tx)).info("Starting OpenAPI server with pre-loaded spec...");
}
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!("OpenAPI server listening on {}", local_addr));
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,
"OpenAPI",
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);
Log::new(Some(&status_tx)).info(format!(
"OpenAPI connection {} from {}",
connection_id, remote_addr
));
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(),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let llm_client_clone = llm_client.clone();
let app_state_clone = app_state.clone();
let status_tx_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let openapi_state_clone = openapi_state.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
let spoke = matches!(
tokio::time::timeout(
FIRST_BYTE_READ_TIMEOUT,
stream.peek(&mut [0u8; 1]),
)
.await,
Ok(Ok(n)) if n > 0
);
let io = TokioIo::new(stream);
let status_for_service = status_tx_clone.clone();
let app_state_for_service = app_state_clone.clone();
let openapi_state_for_service = openapi_state_clone.clone();
let activity = std::sync::Arc::new(
crate::server::accept_bounded::ConnectionActivity::new(),
);
let activity_for_service = std::sync::Arc::clone(&activity);
let service = service_fn(move |req: Request<Incoming>| {
let llm_clone = llm_client_clone.clone();
let state_clone = app_state_for_service.clone();
let status_clone = status_for_service.clone();
let protocol_clone = protocol_clone.clone();
let openapi_state_clone = openapi_state_for_service.clone();
let activity = std::sync::Arc::clone(&activity_for_service);
async move {
let _busy = activity.busy();
handle_openapi_request(
req,
connection_id,
llm_clone,
state_clone,
status_clone,
protocol_clone,
openapi_state_clone,
server_id,
)
.await
}
});
if !spoke {
Log::new(Some(&status_tx_clone)).debug(format!(
"OpenAPI peer {} sent nothing for {}s; closing before \
any request",
remote_addr,
FIRST_BYTE_READ_TIMEOUT.as_secs()
));
} else {
let conn = http1::Builder::new().serve_connection(io, service);
tokio::pin!(conn);
tokio::select! {
result = &mut conn => {
if let Err(err) = result {
error!(
"Error serving OpenAPI connection: \
{:?}",
err
);
}
}
_ = crate::server::accept_bounded::watch_idle(
std::sync::Arc::clone(&activity),
IDLE_BETWEEN_REQUESTS_TIMEOUT,
) => {
Log::new(Some(&status_tx_clone)).debug(format!(
"OpenAPI connection {} idle for {}s; closing",
connection_id,
IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
));
}
}
}
app_state_clone
.close_connection_on_server(server_id, connection_id)
.await;
Log::new(Some(&status_tx_clone))
.info(format!("OpenAPI connection {} closed", connection_id));
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to accept OpenAPI connection: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
async fn handle_openapi_request(
req: Request<Incoming>,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OpenApiProtocol>,
openapi_state: Arc<RwLock<OpenApiState>>,
server_id: crate::state::ServerId,
) -> Result<Response<Full<Bytes>>, Infallible> {
let method = req.method().to_string();
let uri = req.uri().to_string();
let path = req.uri().path().to_string();
let mut headers = HashMap::new();
for (name, value) in req.headers() {
if let Ok(value_str) = value.to_str() {
headers.insert(name.to_string(), value_str.to_string());
}
}
let limited = http_body_util::Limited::new(req.into_body(), MAX_REQUEST_BODY_BYTES);
let body_bytes = match limited.collect().await {
Ok(collected) => collected.to_bytes(),
Err(e) => {
Log::new(Some(&status_tx)).warn(format!(
"OpenAPI {} {} decision=fail_closed_body_too_large: {} (limit {} bytes)",
method, path, e, MAX_REQUEST_BODY_BYTES
));
return Ok(payload_too_large());
}
};
Log::new(Some(&status_tx)).debug(format!(
"OpenAPI {} {} ({} bytes)",
method,
path,
body_bytes.len()
));
let log = Log::new(Some(&status_tx));
log.trace("OpenAPI request headers:");
for (name, value) in &headers {
log.trace(format!("OpenAPI header: {}: {}", name, value));
}
if !body_bytes.is_empty() {
if let Ok(body_str) = std::str::from_utf8(&body_bytes) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(body_str) {
let pretty = serde_json::to_string_pretty(&json).unwrap_or(body_str.to_string());
log.trace(format!("OpenAPI request body (JSON):\n{}", pretty));
} else {
log.trace(format!("OpenAPI request body:\n{}", body_str));
}
} else {
log.trace(format!(
"OpenAPI request body (binary): {} bytes",
body_bytes.len()
));
}
}
let (spec_info, match_result, llm_on_invalid) = {
let state = openapi_state.read().await;
let spec_info = json!({
"spec_loaded": state.spec.is_some(),
"spec_valid": state.spec_valid
});
{
let match_result = if let Some(router) = state.router.as_ref() {
Some(match_route(router, &method, &path))
} else {
None
};
(spec_info, match_result, state.llm_on_invalid)
}
};
if let Some(match_result) = match_result {
match match_result {
MatchResult::Found { metadata, params } => {
if !llm_on_invalid {
let body_text = String::from_utf8_lossy(&body_bytes);
if let Err(e) = validate_request(
&metadata.operation_json,
&method,
&path,
&headers,
&body_text,
) {
Log::new(Some(&status_tx)).warn(format!("OpenAPI validation error: {}", e));
return Ok(immediate_400(e.to_string()));
}
}
let body_text = String::from_utf8_lossy(&body_bytes);
let event = Event::new(
&*crate::server::openapi::actions::OPENAPI_REQUEST_EVENT,
serde_json::json!({
"method": method,
"path": path,
"uri": uri,
"headers": headers,
"body": if body_text.is_empty() { "" } else { body_text.as_ref() },
"spec_info": spec_info,
"matched_route": {
"operation_id": metadata.operation_id,
"path_template": metadata.path_template,
"path_params": params,
"operation": metadata.operation_json,
}
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
return handle_llm_response(
execution_result,
status_tx,
openapi_state,
method,
path,
)
.await;
}
Err(e) => {
return Ok(llm_error_response(e, &method, &path, &status_tx));
}
}
}
MatchResult::MethodNotAllowed { allowed_methods } => {
if llm_on_invalid {
debug!(
"OpenAPI 405 Method Not Allowed (LLM will handle): {} {}",
method, path
);
} else {
Log::new(Some(&status_tx)).info(format!(
"OpenAPI 405 Method Not Allowed: {} {} (allowed: {})",
method,
path,
allowed_methods.join(", ")
));
return Ok(immediate_405(allowed_methods));
}
}
MatchResult::NotFound => {
if llm_on_invalid {
debug!(
"OpenAPI 404 Not Found (LLM will handle): {} {}",
method, path
);
} else {
Log::new(Some(&status_tx))
.info(format!("OpenAPI 404 Not Found: {} {}", method, path));
return Ok(immediate_404());
}
}
}
}
let body_text = String::from_utf8_lossy(&body_bytes);
let event = Event::new(
&*crate::server::openapi::actions::OPENAPI_REQUEST_EVENT,
serde_json::json!({
"method": method,
"path": path,
"uri": uri,
"headers": headers,
"body": if body_text.is_empty() { "" } else { body_text.as_ref() },
"spec_info": spec_info
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
handle_llm_response(execution_result, status_tx, openapi_state, method, path).await
}
Err(e) => Ok(llm_error_response(e, &method, &path, &status_tx)),
}
}