klieo-ops-api 2.2.0

Read-only HTTP monitoring router for klieo agentic systems.
Documentation
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
//! `klieo-ops-api` — read-only HTTP monitoring router for klieo agents.
//!
//! Mount the router returned by [`OpsRouterBuilder::build`] at `/_ops`
//! to expose run history, A2A task state, and MCP stream state.
//!
//! All three backing stores are optional. Endpoints whose store is not
//! wired return `501 Not Implemented`.

mod auth;
mod error;
pub mod handlers;
mod state;
pub mod types;

use crate::state::OpsState;
use axum::{middleware, Router};
use klieo_a2a::task_store::A2aTaskStore;
use klieo_auth_common::Authenticator;
use klieo_core::resume::KvResumeBuffer;
use klieo_runlog::RunLogStore;
use std::sync::Arc;

/// Error returned by [`OpsRouterBuilder::build`].
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum OpsBuilderError {
    /// [`OpsRouterBuilder::with_authenticator`] was not called before [`OpsRouterBuilder::build`].
    #[error("authenticator not configured — call with_authenticator() before build()")]
    MissingAuthenticator,
}

/// Builds the `/_ops` monitoring router.
pub struct OpsRouterBuilder {
    authenticator: Option<Arc<dyn Authenticator>>,
    run_log_store: Option<Arc<dyn RunLogStore>>,
    task_store: Option<Arc<A2aTaskStore>>,
    resume_buffer: Option<Arc<KvResumeBuffer>>,
}

impl OpsRouterBuilder {
    /// Start an empty builder. [`Self::build`] returns
    /// [`OpsBuilderError::MissingAuthenticator`] until
    /// [`Self::with_authenticator`] or [`Self::with_dev_auth`] is called.
    pub fn new() -> Self {
        Self {
            authenticator: None,
            run_log_store: None,
            task_store: None,
            resume_buffer: None,
        }
    }

    /// Required — [`Self::build`] returns `Err` if not called.
    pub fn with_authenticator(mut self, auth: Arc<dyn Authenticator>) -> Self {
        self.authenticator = Some(auth);
        self
    }

    /// Capability-shaped default for laptop-dev — wires
    /// [`klieo_auth_common::AllowAnonymous`] so every request reaches
    /// the `/_ops` router without credentials.
    ///
    /// **TEST FIXTURE / DEMO ONLY.** Gated behind the `dev-auth`
    /// feature (CWE-1188); production builds without that feature
    /// cannot reach `AllowAnonymous` and fail to compile rather than
    /// silently accepting any caller. Pair with a non-production
    /// network boundary (loopback bind, dev container) — never expose
    /// a dev-auth `/_ops` to a multi-tenant network.
    #[cfg(feature = "dev-auth")]
    pub fn with_dev_auth(self) -> Self {
        self.with_authenticator(Arc::new(klieo_auth_common::AllowAnonymous))
    }

    /// Enables `GET /runs`, `GET /runs/{id}`, `GET /agents`.
    pub fn with_run_log_store(mut self, store: Arc<dyn RunLogStore>) -> Self {
        self.run_log_store = Some(store);
        self
    }

    /// Enables `GET /tasks`.
    pub fn with_task_store(mut self, store: Arc<A2aTaskStore>) -> Self {
        self.task_store = Some(store);
        self
    }

    /// Enables `GET /streams`.
    pub fn with_resume_buffer(mut self, buf: Arc<KvResumeBuffer>) -> Self {
        self.resume_buffer = Some(buf);
        self
    }

    /// # Errors
    /// Returns [`OpsBuilderError::MissingAuthenticator`] if
    /// [`Self::with_authenticator`] was not called before `build`.
    pub fn build(self) -> Result<Router, OpsBuilderError> {
        let authenticator = self
            .authenticator
            .ok_or(OpsBuilderError::MissingAuthenticator)?;

        let state = Arc::new(OpsState {
            authenticator,
            run_log_store: self.run_log_store,
            task_store: self.task_store,
            resume_buffer: self.resume_buffer,
        });

        use crate::handlers::{runs, streams, tasks};
        use axum::routing::get;

        Ok(Router::new()
            .route("/runs", get(runs::list_runs))
            .route("/runs/:run_id", get(runs::get_run))
            .route("/agents", get(runs::list_agents))
            .route("/tasks", get(tasks::list_tasks))
            .route("/streams", get(streams::list_streams))
            .layer(middleware::from_fn_with_state(
                Arc::clone(&state),
                crate::auth::require_auth,
            ))
            .with_state(state))
    }
}

impl Default for OpsRouterBuilder {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn build_without_authenticator_returns_missing_authenticator() {
        let err = OpsRouterBuilder::new().build().unwrap_err();
        assert!(matches!(err, OpsBuilderError::MissingAuthenticator));
    }

    #[cfg(feature = "dev-auth")]
    #[test]
    fn with_dev_auth_satisfies_authenticator_requirement() {
        let result = OpsRouterBuilder::new().with_dev_auth().build();
        assert!(
            result.is_ok(),
            "with_dev_auth must produce a buildable router"
        );
    }
}