symbi-runtime 1.0.0

Agent Runtime System for the Symbi platform
//! HTTP API server implementation
//!
//! This module provides the main HTTP server implementation using Axum.

#[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;

/// OpenAPI documentation structure
#[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;

/// HTTP API Server configuration
#[cfg(feature = "http-api")]
#[derive(Debug, Clone)]
pub struct HttpApiConfig {
    /// Server bind address
    pub bind_address: String,
    /// Server port
    pub port: u16,
    /// Enable CORS
    pub enable_cors: bool,
    /// Enable request tracing
    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,
        }
    }
}

/// HTTP API Server
#[cfg(feature = "http-api")]
pub struct HttpApiServer {
    config: HttpApiConfig,
    runtime_provider: Option<Arc<dyn RuntimeApiProvider>>,
    start_time: Instant,
}

#[cfg(feature = "http-api")]
impl HttpApiServer {
    /// Create a new HTTP API server instance
    pub fn new(config: HttpApiConfig) -> Self {
        Self {
            config,
            runtime_provider: None,
            start_time: Instant::now(),
        }
    }

    /// Set the runtime provider for the API server
    pub fn with_runtime_provider(mut self, provider: Arc<dyn RuntimeApiProvider>) -> Self {
        self.runtime_provider = Some(provider);
        self
    }

    /// Start the HTTP API server
    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(())
    }

    /// Create the Axum router with all routes and middleware
    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);

        // Add Swagger UI
        router = router
            .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()));

        // Add stateful routes if we have a runtime provider
        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;

            // Agent routes that require authentication
            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());

            // Schedule routes
            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());

            // Other routes with authentication
            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);
        }

        // Add middleware conditionally
        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);
        }

        // Apply security headers to all responses
        router = router.layer(axum::middleware::from_fn(
            crate::api::middleware::security_headers_middleware,
        ));

        router
    }
}

/// Health check endpoint handler
#[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))
}