klieo-ops-api 0.41.3

Read-only HTTP monitoring router for klieo agentic systems.
Documentation
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use serde::Serialize;

/// Stable error codes returned in the JSON body of every non-2xx response.
#[non_exhaustive]
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
    StoreNotConfigured,
    RunNotFound,
    ContextIdRequired,
    Unauthorized,
    Forbidden,
    Internal,
    InvalidFilter,
}

#[derive(Debug, Serialize)]
struct ErrorBody {
    error: ErrorCode,
}

#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum OpsError {
    #[error("backing store not configured for this endpoint")]
    StoreNotConfigured,
    #[error("run not found")]
    RunNotFound,
    #[error("context_id parameter is required")]
    ContextIdRequired,
    #[error("unauthorized")]
    Unauthorized,
    #[error("forbidden")]
    Forbidden,
    #[error("internal error")]
    Internal(#[source] Box<dyn std::error::Error + Send + Sync>),
    #[error("invalid filter value")]
    InvalidFilter,
}

impl IntoResponse for OpsError {
    fn into_response(self) -> Response {
        let (status, code) = match self {
            OpsError::StoreNotConfigured => (StatusCode::NOT_IMPLEMENTED, ErrorCode::StoreNotConfigured),
            OpsError::RunNotFound => (StatusCode::NOT_FOUND, ErrorCode::RunNotFound),
            OpsError::ContextIdRequired => (StatusCode::BAD_REQUEST, ErrorCode::ContextIdRequired),
            OpsError::Unauthorized => (StatusCode::UNAUTHORIZED, ErrorCode::Unauthorized),
            OpsError::Forbidden => (StatusCode::FORBIDDEN, ErrorCode::Forbidden),
            OpsError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Internal),
            OpsError::InvalidFilter => (StatusCode::BAD_REQUEST, ErrorCode::InvalidFilter),
        };
        (status, Json(ErrorBody { error: code })).into_response()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn store_not_configured_serialises_snake_case() {
        let body = ErrorBody { error: ErrorCode::StoreNotConfigured };
        let json = serde_json::to_string(&body).unwrap();
        assert!(json.contains("store_not_configured"), "got: {json}");
    }

    #[test]
    fn internal_error_maps_to_500() {
        use std::error::Error;
        use std::io;
        let err = io::Error::other("db timeout");
        let ops_err = OpsError::Internal(Box::new(err));
        assert!(ops_err.source().is_some(), "source chain must be retained");
        assert!(ops_err.source().unwrap().to_string().contains("db timeout"));
        let resp = ops_err.into_response();
        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
    }
}