#[cfg(feature = "http-api")]
use axum::{http::StatusCode, response::Json, Router};
#[cfg(feature = "http-api")]
use std::sync::Arc;
#[cfg(feature = "http-api")]
use std::time::Instant;
#[cfg(feature = "http-api")]
use tokio::net::TcpListener;
#[cfg(feature = "http-api")]
use tower_http::{cors::CorsLayer, trace::TraceLayer};
#[cfg(feature = "http-api")]
use utoipa::OpenApi;
#[cfg(feature = "http-api")]
use utoipa_swagger_ui::SwaggerUi;
#[cfg(feature = "http-api")]
use super::types::{
AgentExecutionRecord, AgentStatusResponse, CreateAgentRequest, CreateAgentResponse,
CreateScheduleRequest, CreateScheduleResponse, DeleteAgentResponse, DeleteScheduleResponse,
ErrorResponse, ExecuteAgentRequest, ExecuteAgentResponse, GetAgentHistoryResponse,
HealthResponse, NextRunsResponse, ResourceUsage, ScheduleActionResponse, ScheduleDetail,
ScheduleHistoryResponse, ScheduleRunEntry, ScheduleSummary, SchedulerHealthResponse,
UpdateAgentRequest, UpdateAgentResponse, UpdateScheduleRequest, WorkflowExecutionRequest,
};
#[cfg(feature = "http-api")]
use super::traits::RuntimeApiProvider;
#[cfg(feature = "http-api")]
use crate::types::RuntimeError;
#[cfg(feature = "http-api")]
#[derive(OpenApi)]
#[openapi(
paths(
super::routes::execute_workflow,
super::routes::get_agent_status,
super::routes::list_agents,
super::routes::get_metrics,
super::routes::create_agent,
super::routes::update_agent,
super::routes::delete_agent,
super::routes::execute_agent,
super::routes::get_agent_history,
super::routes::list_schedules,
super::routes::create_schedule,
super::routes::get_schedule,
super::routes::update_schedule,
super::routes::delete_schedule,
super::routes::pause_schedule,
super::routes::resume_schedule,
super::routes::trigger_schedule,
super::routes::get_schedule_history,
super::routes::get_schedule_next_runs,
super::routes::get_scheduler_health,
health_check
),
components(
schemas(
WorkflowExecutionRequest,
AgentStatusResponse,
ResourceUsage,
HealthResponse,
CreateAgentRequest,
CreateAgentResponse,
UpdateAgentRequest,
UpdateAgentResponse,
DeleteAgentResponse,
ExecuteAgentRequest,
ExecuteAgentResponse,
GetAgentHistoryResponse,
AgentExecutionRecord,
ErrorResponse,
CreateScheduleRequest,
CreateScheduleResponse,
UpdateScheduleRequest,
ScheduleSummary,
ScheduleDetail,
NextRunsResponse,
ScheduleRunEntry,
ScheduleHistoryResponse,
ScheduleActionResponse,
DeleteScheduleResponse,
SchedulerHealthResponse
)
),
tags(
(name = "agents", description = "Agent management endpoints"),
(name = "workflows", description = "Workflow execution endpoints"),
(name = "system", description = "System monitoring and health endpoints"),
(name = "schedules", description = "Cron schedule management endpoints")
),
info(
title = "Symbiont Runtime API",
description = "HTTP API for the Symbiont Agent Runtime System",
version = "1.0.0",
contact(
name = "ThirdKey.ai",
url = "https://github.com/thirdkeyai/symbiont"
),
license(
name = "MIT",
url = "https://opensource.org/licenses/MIT"
)
)
)]
pub struct ApiDoc;
#[cfg(feature = "http-api")]
#[derive(Debug, Clone)]
pub struct HttpApiConfig {
pub bind_address: String,
pub port: u16,
pub enable_cors: bool,
pub enable_tracing: bool,
}
#[cfg(feature = "http-api")]
impl Default for HttpApiConfig {
fn default() -> Self {
Self {
bind_address: "127.0.0.1".to_string(),
port: 8080,
enable_cors: true,
enable_tracing: true,
}
}
}
#[cfg(feature = "http-api")]
pub struct HttpApiServer {
config: HttpApiConfig,
runtime_provider: Option<Arc<dyn RuntimeApiProvider>>,
start_time: Instant,
}
#[cfg(feature = "http-api")]
impl HttpApiServer {
pub fn new(config: HttpApiConfig) -> Self {
Self {
config,
runtime_provider: None,
start_time: Instant::now(),
}
}
pub fn with_runtime_provider(mut self, provider: Arc<dyn RuntimeApiProvider>) -> Self {
self.runtime_provider = Some(provider);
self
}
pub async fn start(&self) -> Result<(), RuntimeError> {
let app = self.create_router();
let addr = format!("{}:{}", self.config.bind_address, self.config.port);
let listener = TcpListener::bind(&addr)
.await
.map_err(|e| RuntimeError::Internal(format!("Failed to bind to {}: {}", addr, e)))?;
tracing::info!("HTTP API server starting on {}", addr);
axum::serve(listener, app)
.await
.map_err(|e| RuntimeError::Internal(format!("Server error: {}", e)))?;
Ok(())
}
fn create_router(&self) -> Router {
use axum::routing::{get, post, put};
let mut router = Router::new()
.route("/api/v1/health", get(health_check))
.with_state(self.start_time);
router = router
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()));
if let Some(provider) = &self.runtime_provider {
use super::middleware::auth_middleware;
use super::routes::{
create_agent, create_schedule, delete_agent, delete_schedule, execute_agent,
execute_workflow, get_agent_history, get_agent_status, get_metrics, get_schedule,
get_schedule_history, get_schedule_next_runs, get_scheduler_health, list_agents,
list_schedules, pause_schedule, resume_schedule, trigger_schedule, update_agent,
update_schedule,
};
use axum::middleware;
let agent_router = Router::new()
.route("/api/v1/agents", get(list_agents).post(create_agent))
.route("/api/v1/agents/:id/status", get(get_agent_status))
.route("/api/v1/agents/:id", put(update_agent).delete(delete_agent))
.route("/api/v1/agents/:id/execute", post(execute_agent))
.route("/api/v1/agents/:id/history", get(get_agent_history))
.layer(middleware::from_fn(auth_middleware))
.with_state(provider.clone());
let schedule_router = Router::new()
.route(
"/api/v1/schedules",
get(list_schedules).post(create_schedule),
)
.route(
"/api/v1/schedules/:id",
get(get_schedule)
.put(update_schedule)
.delete(delete_schedule),
)
.route("/api/v1/schedules/:id/pause", post(pause_schedule))
.route("/api/v1/schedules/:id/resume", post(resume_schedule))
.route("/api/v1/schedules/:id/trigger", post(trigger_schedule))
.route("/api/v1/schedules/:id/history", get(get_schedule_history))
.route(
"/api/v1/schedules/:id/next-runs",
get(get_schedule_next_runs),
)
.layer(middleware::from_fn(auth_middleware))
.with_state(provider.clone());
let other_router = Router::new()
.route("/api/v1/workflows/execute", post(execute_workflow))
.route("/api/v1/metrics", get(get_metrics))
.route("/api/v1/health/scheduler", get(get_scheduler_health))
.layer(middleware::from_fn(auth_middleware))
.with_state(provider.clone());
router = router
.merge(agent_router)
.merge(schedule_router)
.merge(other_router);
}
if self.config.enable_tracing {
router = router.layer(TraceLayer::new_for_http());
}
if self.config.enable_cors {
use axum::http::{header, HeaderValue, Method};
let allowed_origins: Vec<HeaderValue> = std::env::var("SYMBIONT_CORS_ORIGINS")
.unwrap_or_else(|_| "http://localhost:3000".to_string())
.split(',')
.filter_map(|origin| origin.trim().parse().ok())
.collect();
let cors = CorsLayer::new()
.allow_origin(allowed_origins)
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE])
.allow_credentials(false);
router = router.layer(cors);
}
router = router.layer(axum::middleware::from_fn(
crate::api::middleware::security_headers_middleware,
));
router
}
}
#[cfg(feature = "http-api")]
#[utoipa::path(
get,
path = "/api/v1/health",
responses(
(status = 200, description = "Health check successful", body = HealthResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
tag = "system"
)]
async fn health_check(
axum::extract::State(start_time): axum::extract::State<Instant>,
) -> Result<Json<HealthResponse>, (StatusCode, Json<ErrorResponse>)> {
let uptime_seconds = start_time.elapsed().as_secs();
let response = HealthResponse {
status: "healthy".to_string(),
uptime_seconds,
timestamp: chrono::Utc::now(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
Ok(Json(response))
}