1use std::collections::HashMap;
18
19use serde::{Deserialize, Serialize};
20
21use crate::governance::{
22 EffectVector, GovernanceDecision, GovernanceEngine, GovernanceRequest, GovernanceResult,
23};
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ExecuteRequest {
30 pub agent_id: String,
32
33 pub task: String,
35
36 #[serde(default)]
38 pub context: HashMap<String, serde_json::Value>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ExecuteResponse {
44 pub result: String,
46
47 pub score: f64,
49
50 pub tokens_used: u64,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub execution_id: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct GovernRequest {
61 pub action: String,
63
64 pub agent_id: String,
66
67 #[serde(default)]
69 pub context: HashMap<String, serde_json::Value>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct GovernResponse {
75 pub decision: String,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub attestation: Option<String>,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub chain_hash: Option<String>,
85
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub evaluated_rules: Vec<String>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub effect_magnitude: Option<f64>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct HealthResponse {
98 pub status: String,
100
101 pub version: String,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub uptime_secs: Option<u64>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ErrorResponse {
112 pub code: String,
114
115 pub message: String,
117}
118
119pub struct HttpApiHandler {
135 governance: GovernanceEngine,
136}
137
138impl HttpApiHandler {
139 pub fn new(governance: GovernanceEngine) -> Self {
141 Self { governance }
142 }
143
144 pub fn handle_execute(&self, req: &ExecuteRequest) -> Result<ExecuteResponse, ErrorResponse> {
154 if req.agent_id.is_empty() {
155 return Err(ErrorResponse {
156 code: "INVALID_AGENT_ID".into(),
157 message: "agent_id must not be empty".into(),
158 });
159 }
160 if req.task.is_empty() {
161 return Err(ErrorResponse {
162 code: "INVALID_TASK".into(),
163 message: "task must not be empty".into(),
164 });
165 }
166
167 let execution_id = uuid::Uuid::new_v4().to_string();
171 Ok(ExecuteResponse {
172 result: format!("Task accepted for agent '{}'", req.agent_id),
173 score: 0.0,
174 tokens_used: 0,
175 execution_id: Some(execution_id),
176 })
177 }
178
179 pub fn handle_govern(&self, req: &GovernRequest) -> Result<GovernResponse, ErrorResponse> {
185 if req.agent_id.is_empty() {
186 return Err(ErrorResponse {
187 code: "INVALID_AGENT_ID".into(),
188 message: "agent_id must not be empty".into(),
189 });
190 }
191 if req.action.is_empty() {
192 return Err(ErrorResponse {
193 code: "INVALID_ACTION".into(),
194 message: "action must not be empty".into(),
195 });
196 }
197
198 let mut gov_req = GovernanceRequest::new(&req.agent_id, &req.action);
200
201 for (k, v) in &req.context {
203 if let Some(s) = v.as_str() {
204 gov_req = gov_req.with_context_entry(k, s);
205 }
206 }
207
208 let effect = extract_effect_vector(&req.context);
210 gov_req = gov_req.with_effect(effect);
211
212 let result: GovernanceResult = self.governance.evaluate(&gov_req);
213
214 let decision_str = match &result.decision {
215 GovernanceDecision::Permit | GovernanceDecision::PermitWithWarning(_) => {
216 "permit".to_string()
217 }
218 GovernanceDecision::EscalateToHuman(_) => "escalate".to_string(),
219 GovernanceDecision::Deny(_) => "deny".to_string(),
220 };
221
222 Ok(GovernResponse {
223 decision: decision_str,
224 attestation: None, chain_hash: None,
226 evaluated_rules: result.evaluated_rules,
227 effect_magnitude: Some(result.effect.magnitude()),
228 })
229 }
230
231 pub fn handle_health(&self) -> HealthResponse {
233 HealthResponse {
234 status: "ok".into(),
235 version: env!("CARGO_PKG_VERSION").into(),
236 uptime_secs: None,
237 }
238 }
239}
240
241fn extract_effect_vector(context: &HashMap<String, serde_json::Value>) -> EffectVector {
246 let f = |key: &str| -> f64 { context.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0) };
247 EffectVector {
248 risk: f("risk"),
249 fairness: f("fairness"),
250 privacy: f("privacy"),
251 novelty: f("novelty"),
252 security: f("security"),
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
260pub enum Route {
261 Execute,
263 Govern,
265 Health,
267 NotFound,
269}
270
271pub fn match_route(method: &str, path: &str) -> Route {
273 match (method, path) {
274 ("POST", "/api/v1/execute") => Route::Execute,
275 ("POST", "/api/v1/govern") => Route::Govern,
276 ("GET", "/api/v1/health") => Route::Health,
277 _ => Route::NotFound,
278 }
279}
280
281pub fn dispatch(handler: &HttpApiHandler, route: &Route, body: &[u8]) -> Result<Vec<u8>, Vec<u8>> {
287 match route {
288 Route::Execute => {
289 let req: ExecuteRequest = serde_json::from_slice(body).map_err(|e| {
290 serde_json::to_vec(&ErrorResponse {
291 code: "PARSE_ERROR".into(),
292 message: format!("Invalid JSON: {e}"),
293 })
294 .unwrap_or_default()
295 })?;
296 match handler.handle_execute(&req) {
297 Ok(resp) => serde_json::to_vec(&resp).map_err(|e| {
298 serde_json::to_vec(&ErrorResponse {
299 code: "INTERNAL".into(),
300 message: e.to_string(),
301 })
302 .unwrap_or_default()
303 }),
304 Err(err) => Err(serde_json::to_vec(&err).unwrap_or_default()),
305 }
306 }
307 Route::Govern => {
308 let req: GovernRequest = serde_json::from_slice(body).map_err(|e| {
309 serde_json::to_vec(&ErrorResponse {
310 code: "PARSE_ERROR".into(),
311 message: format!("Invalid JSON: {e}"),
312 })
313 .unwrap_or_default()
314 })?;
315 match handler.handle_govern(&req) {
316 Ok(resp) => serde_json::to_vec(&resp).map_err(|e| {
317 serde_json::to_vec(&ErrorResponse {
318 code: "INTERNAL".into(),
319 message: e.to_string(),
320 })
321 .unwrap_or_default()
322 }),
323 Err(err) => Err(serde_json::to_vec(&err).unwrap_or_default()),
324 }
325 }
326 Route::Health => {
327 let resp = handler.handle_health();
328 serde_json::to_vec(&resp).map_err(|e| {
329 serde_json::to_vec(&ErrorResponse {
330 code: "INTERNAL".into(),
331 message: e.to_string(),
332 })
333 .unwrap_or_default()
334 })
335 }
336 Route::NotFound => Err(serde_json::to_vec(&ErrorResponse {
337 code: "NOT_FOUND".into(),
338 message: "Unknown route".into(),
339 })
340 .unwrap_or_default()),
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 fn make_handler() -> HttpApiHandler {
349 HttpApiHandler::new(GovernanceEngine::open())
350 }
351
352 #[test]
353 fn test_execute_success() {
354 let handler = make_handler();
355 let req = ExecuteRequest {
356 agent_id: "agent-1".into(),
357 task: "summarise document".into(),
358 context: HashMap::new(),
359 };
360 let resp = handler.handle_execute(&req).unwrap();
361 assert!(resp.result.contains("agent-1"));
362 assert!(resp.execution_id.is_some());
363 }
364
365 #[test]
366 fn test_execute_empty_agent_id() {
367 let handler = make_handler();
368 let req = ExecuteRequest {
369 agent_id: String::new(),
370 task: "do something".into(),
371 context: HashMap::new(),
372 };
373 let err = handler.handle_execute(&req).unwrap_err();
374 assert_eq!(err.code, "INVALID_AGENT_ID");
375 }
376
377 #[test]
378 fn test_execute_empty_task() {
379 let handler = make_handler();
380 let req = ExecuteRequest {
381 agent_id: "a".into(),
382 task: String::new(),
383 context: HashMap::new(),
384 };
385 let err = handler.handle_execute(&req).unwrap_err();
386 assert_eq!(err.code, "INVALID_TASK");
387 }
388
389 #[test]
390 fn test_govern_permit() {
391 let handler = make_handler();
392 let req = GovernRequest {
393 action: "read_file".into(),
394 agent_id: "agent-1".into(),
395 context: HashMap::new(),
396 };
397 let resp = handler.handle_govern(&req).unwrap();
398 assert_eq!(resp.decision, "permit");
399 }
400
401 #[test]
402 fn test_govern_empty_action() {
403 let handler = make_handler();
404 let req = GovernRequest {
405 action: String::new(),
406 agent_id: "a".into(),
407 context: HashMap::new(),
408 };
409 let err = handler.handle_govern(&req).unwrap_err();
410 assert_eq!(err.code, "INVALID_ACTION");
411 }
412
413 #[test]
414 fn test_health() {
415 let handler = make_handler();
416 let resp = handler.handle_health();
417 assert_eq!(resp.status, "ok");
418 assert!(!resp.version.is_empty());
419 }
420
421 #[test]
422 fn test_route_matching() {
423 assert_eq!(match_route("POST", "/api/v1/execute"), Route::Execute);
424 assert_eq!(match_route("POST", "/api/v1/govern"), Route::Govern);
425 assert_eq!(match_route("GET", "/api/v1/health"), Route::Health);
426 assert_eq!(match_route("GET", "/api/v1/execute"), Route::NotFound);
427 assert_eq!(match_route("DELETE", "/unknown"), Route::NotFound);
428 }
429
430 #[test]
431 fn test_dispatch_execute() {
432 let handler = make_handler();
433 let body = serde_json::to_vec(&ExecuteRequest {
434 agent_id: "a1".into(),
435 task: "hello".into(),
436 context: HashMap::new(),
437 })
438 .unwrap();
439 let result = dispatch(&handler, &Route::Execute, &body);
440 assert!(result.is_ok());
441 let resp: ExecuteResponse = serde_json::from_slice(&result.unwrap()).unwrap();
442 assert!(resp.result.contains("a1"));
443 }
444
445 #[test]
446 fn test_dispatch_invalid_json() {
447 let handler = make_handler();
448 let result = dispatch(&handler, &Route::Execute, b"not json");
449 assert!(result.is_err());
450 let err: ErrorResponse = serde_json::from_slice(&result.unwrap_err()).unwrap();
451 assert_eq!(err.code, "PARSE_ERROR");
452 }
453
454 #[test]
455 fn test_dispatch_not_found() {
456 let handler = make_handler();
457 let result = dispatch(&handler, &Route::NotFound, b"{}");
458 assert!(result.is_err());
459 }
460
461 #[test]
462 fn test_effect_vector_extraction() {
463 let mut ctx = HashMap::new();
464 ctx.insert("risk".into(), serde_json::json!(0.7));
465 ctx.insert("security".into(), serde_json::json!(0.3));
466 let ev = extract_effect_vector(&ctx);
467 assert!((ev.risk - 0.7).abs() < f64::EPSILON);
468 assert!((ev.security - 0.3).abs() < f64::EPSILON);
469 assert!((ev.fairness - 0.0).abs() < f64::EPSILON);
470 }
471}