lean_ctx/http_server/
kernel_api.rs1use 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#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
12pub struct EtpaoResponse {
13 pub proxy_etpao: f64,
15 pub mcp_etpao: f64,
17 pub combined_etpao: f64,
19}
20
21#[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#[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#[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#[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#[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#[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#[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#[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}