#[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::{
AddIdentityMappingRequest, AgentExecutionRecord, AgentStatusResponse, ChannelActionResponse,
ChannelAuditEntry, ChannelAuditResponse, ChannelDetail, ChannelHealthResponse, ChannelSummary,
CreateAgentRequest, CreateAgentResponse, CreateScheduleRequest, CreateScheduleResponse,
DeleteAgentResponse, DeleteChannelResponse, DeleteScheduleResponse, ErrorResponse,
ExecuteAgentRequest, ExecuteAgentResponse, GetAgentHistoryResponse, HealthResponse,
IdentityMappingEntry, NextRunsResponse, RegisterChannelRequest, RegisterChannelResponse,
ResourceUsage, ScheduleActionResponse, ScheduleDetail, ScheduleHistoryResponse,
ScheduleRunEntry, ScheduleSummary, SchedulerHealthResponse, UpdateAgentRequest,
UpdateAgentResponse, UpdateChannelRequest, 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,
super::routes::list_channels,
super::routes::register_channel,
super::routes::get_channel,
super::routes::update_channel,
super::routes::delete_channel,
super::routes::start_channel,
super::routes::stop_channel,
super::routes::get_channel_health,
super::routes::list_channel_mappings,
super::routes::add_channel_mapping,
super::routes::remove_channel_mapping,
super::routes::get_channel_audit,
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,
RegisterChannelRequest,
RegisterChannelResponse,
UpdateChannelRequest,
ChannelSummary,
ChannelDetail,
ChannelActionResponse,
DeleteChannelResponse,
ChannelHealthResponse,
IdentityMappingEntry,
AddIdentityMappingRequest,
ChannelAuditEntry,
ChannelAuditResponse
)
),
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"),
(name = "channels", description = "Channel adapter 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::{delete, 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::{
add_channel_mapping, create_agent, create_schedule, delete_agent, delete_channel,
delete_schedule, execute_agent, execute_workflow, get_agent_history,
get_agent_status, get_channel, get_channel_audit, get_channel_health, get_metrics,
get_schedule, get_schedule_history, get_schedule_next_runs, get_scheduler_health,
list_agents, list_channel_mappings, list_channels, list_schedules, pause_schedule,
register_channel, remove_channel_mapping, resume_schedule, start_channel,
stop_channel, trigger_schedule, update_agent, update_channel, 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 channel_router = Router::new()
.route(
"/api/v1/channels",
get(list_channels).post(register_channel),
)
.route(
"/api/v1/channels/:id",
get(get_channel).put(update_channel).delete(delete_channel),
)
.route("/api/v1/channels/:id/start", post(start_channel))
.route("/api/v1/channels/:id/stop", post(stop_channel))
.route("/api/v1/channels/:id/health", get(get_channel_health))
.route(
"/api/v1/channels/:id/mappings",
get(list_channel_mappings).post(add_channel_mapping),
)
.route(
"/api/v1/channels/:id/mappings/:user_id",
delete(remove_channel_mapping),
)
.route("/api/v1/channels/:id/audit", get(get_channel_audit))
.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(channel_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))
}