Skip to main content

lean_ctx/http_server/
kernel_api.rs

1//! HTTP handlers exposing Context Kernel runtime state.
2
3use axum::{Json, http::StatusCode};
4use serde::{Deserialize, Serialize};
5
6use crate::core::context_kernel::{
7    envelope_wiring, kernel_config, live_dashboard, mcp_bridge, proxy_bridge,
8};
9
10/// Point-in-time ETPAO values for the proxy and MCP hot paths.
11#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
12pub struct EtpaoResponse {
13    /// Current proxy effective tokens per accepted outcome.
14    pub proxy_etpao: f64,
15    /// Current MCP effective tokens per accepted outcome.
16    pub mcp_etpao: f64,
17    /// Arithmetic mean of the proxy and MCP ETPAO values.
18    pub combined_etpao: f64,
19}
20
21/// Returns the live Context Kernel dashboard snapshot.
22#[allow(clippy::unused_async)]
23pub async fn dashboard() -> Json<serde_json::Value> {
24    let snapshot = live_dashboard::snapshot_json();
25    let report = crate::core::context_kernel::dashboard_report::generate_report();
26    let mut base = serde_json::from_str::<serde_json::Value>(&snapshot)
27        .unwrap_or_else(|_| serde_json::json!({}));
28    if let (Some(obj), Ok(report_val)) = (base.as_object_mut(), serde_json::to_value(&report)) {
29        obj.insert("report".to_string(), report_val);
30    }
31    Json(base)
32}
33
34/// Returns current ETPAO values for both active integration paths.
35#[allow(clippy::unused_async)]
36pub async fn etpao() -> Json<EtpaoResponse> {
37    let proxy_etpao = proxy_bridge::current_etpao();
38    let mcp_etpao = mcp_bridge::mcp_etpao();
39    Json(EtpaoResponse {
40        proxy_etpao,
41        mcp_etpao,
42        combined_etpao: f64::midpoint(proxy_etpao, mcp_etpao),
43    })
44}
45
46/// Returns the current Context Kernel runtime feature configuration.
47#[allow(clippy::unused_async)]
48pub async fn get_config() -> Json<serde_json::Value> {
49    Json(
50        serde_json::to_value(kernel_config::features()).unwrap_or_else(|error| {
51            serde_json::json!({
52                "error": "invalid kernel configuration",
53                "detail": error.to_string(),
54            })
55        }),
56    )
57}
58
59/// Replaces the Context Kernel runtime feature configuration.
60#[allow(clippy::unused_async)]
61pub async fn set_config(
62    Json(body): Json<serde_json::Value>,
63) -> Result<Json<serde_json::Value>, StatusCode> {
64    let features = serde_json::from_value(body).map_err(|_| StatusCode::BAD_REQUEST)?;
65    kernel_config::update_features(features);
66    Ok(get_config().await)
67}
68
69/// Returns aggregate evidence from the active Context Kernel pipeline.
70#[allow(clippy::unused_async)]
71pub async fn evidence() -> Json<serde_json::Value> {
72    Json(
73        serde_json::to_value(envelope_wiring::evidence_summary()).unwrap_or_else(|error| {
74            serde_json::json!({
75                "error": "invalid kernel evidence",
76                "detail": error.to_string(),
77            })
78        }),
79    )
80}
81
82/// Clears live kernel evidence and ETPAO state.
83#[allow(clippy::unused_async)]
84pub async fn reset_state() -> Json<&'static str> {
85    envelope_wiring::reset_evidence();
86    proxy_bridge::reset_state();
87    mcp_bridge::reset_mcp_state();
88    Json("ok")
89}
90
91/// Returns the aggregated Context Kernel health report.
92#[allow(clippy::unused_async)]
93pub async fn health() -> Json<serde_json::Value> {
94    let json_str = crate::core::context_kernel::health_api::health_json();
95    Json(serde_json::from_str(&json_str).unwrap_or_else(|error| {
96        serde_json::json!({
97            "error": "invalid health snapshot",
98            "detail": error.to_string(),
99        })
100    }))
101}
102
103/// Returns a structured kernel dashboard report.
104#[allow(clippy::unused_async)]
105pub async fn report() -> Json<serde_json::Value> {
106    let r = crate::core::context_kernel::dashboard_report::generate_report();
107    Json(serde_json::to_value(&r).unwrap_or_else(|e| serde_json::json!({ "error": e.to_string() })))
108}
109
110#[cfg(test)]
111mod tests {
112    use axum::{body::to_bytes, response::IntoResponse};
113
114    use super::*;
115
116    async fn response_json(response: impl IntoResponse) -> serde_json::Value {
117        let response = response.into_response();
118        let bytes = to_bytes(response.into_body(), 1_000_000)
119            .await
120            .expect("response body should be readable");
121        serde_json::from_slice(&bytes).expect("response body should contain JSON")
122    }
123
124    #[tokio::test]
125    async fn dashboard_returns_valid_json() {
126        let value = response_json(dashboard().await).await;
127        assert!(value.is_object());
128    }
129
130    #[tokio::test]
131    async fn etpao_returns_numbers() {
132        let value = response_json(etpao().await).await;
133        assert!(value["proxy_etpao"].is_f64());
134        assert!(value["mcp_etpao"].is_f64());
135        assert!(value["combined_etpao"].is_f64());
136    }
137
138    #[tokio::test]
139    #[allow(clippy::await_holding_lock)]
140    async fn config_roundtrip() {
141        let _guard = kernel_config::KERNEL_TEST_LOCK
142            .lock()
143            .unwrap_or_else(std::sync::PoisonError::into_inner);
144        let original = response_json(get_config().await).await;
145        let mut modified = original.clone();
146        modified["content_dedup"] = serde_json::Value::Bool(
147            !original["content_dedup"]
148                .as_bool()
149                .expect("content_dedup should be boolean"),
150        );
151
152        let response = set_config(Json(modified.clone()))
153            .await
154            .expect("valid configuration should be accepted");
155        assert_eq!(response_json(response).await, modified);
156        assert_eq!(response_json(get_config().await).await, modified);
157
158        let _ = set_config(Json(original))
159            .await
160            .expect("original configuration should be restored");
161    }
162
163    #[tokio::test]
164    async fn evidence_returns_summary() {
165        let value = response_json(evidence().await).await;
166        for field in [
167            "proxy_requests",
168            "mcp_calls",
169            "total_envelopes",
170            "chain_entries",
171            "compression_ratio",
172            "kernel_hit_rate",
173        ] {
174            assert!(value.get(field).is_some(), "missing field: {field}");
175        }
176    }
177}