chio_http_core/plan.rs
1//! Phase 2.4 plan-level evaluation HTTP surface.
2//!
3//! Mirrors the structure of [`crate::emergency`]: `chio-http-core` does
4//! not embed an HTTP server, so this module exposes a substrate-agnostic
5//! handler that accepts raw request bytes, delegates to the kernel, and
6//! returns a structured response. Each substrate adapter wires the
7//! handler into its own framework-native route handler.
8//!
9//! Unlike the emergency endpoints, `/evaluate-plan` does NOT require an
10//! admin token: any caller in possession of a valid capability token
11//! can ask the kernel whether a prospective plan would be allowed. The
12//! pre-flight check is explicitly designed to be consulted often by
13//! agent planners during plan generation.
14//!
15//! The handler returns `200 OK` regardless of the aggregate plan
16//! verdict: denials are conveyed inside the JSON body, not as HTTP
17//! status codes. Only malformed request bodies produce a 400.
18
19use std::sync::Arc;
20
21use chio_core_types::{PlanEvaluationRequest, PlanEvaluationResponse};
22use chio_kernel::ChioKernel;
23use serde::{Deserialize, Serialize};
24
25/// Error surfaced by [`handle_evaluate_plan`] when the request body is
26/// malformed. Aggregate plan denials are NOT represented here; those
27/// are carried inside the successful response.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub enum PlanHandlerError {
30 /// Request body could not be parsed as a `PlanEvaluationRequest`.
31 BadRequest(String),
32}
33
34impl PlanHandlerError {
35 /// HTTP status code for this error. Always 400 in v1.
36 #[must_use]
37 pub fn status(&self) -> u16 {
38 match self {
39 Self::BadRequest(_) => 400,
40 }
41 }
42
43 /// Stable machine-readable error code.
44 #[must_use]
45 pub fn code(&self) -> &'static str {
46 match self {
47 Self::BadRequest(_) => "bad_request",
48 }
49 }
50
51 /// Human-readable message.
52 #[must_use]
53 pub fn message(&self) -> String {
54 match self {
55 Self::BadRequest(reason) => reason.clone(),
56 }
57 }
58
59 /// Wire body for this error response, matching the emergency-
60 /// handler error shape so adapters can reuse their serialisation.
61 #[must_use]
62 pub fn body(&self) -> serde_json::Value {
63 serde_json::json!({
64 "error": self.code(),
65 "message": self.message(),
66 })
67 }
68}
69
70/// Handler for `POST /evaluate-plan`.
71///
72/// Takes the raw request body as bytes, parses it into a
73/// [`PlanEvaluationRequest`], and returns the kernel's
74/// [`PlanEvaluationResponse`]. Denials are always `Ok`: the HTTP layer
75/// only errors out on malformed bodies.
76pub fn handle_evaluate_plan(
77 kernel: &Arc<ChioKernel>,
78 body: &[u8],
79) -> Result<PlanEvaluationResponse, PlanHandlerError> {
80 let parsed: PlanEvaluationRequest = serde_json::from_slice(body).map_err(|error| {
81 PlanHandlerError::BadRequest(format!("invalid evaluate-plan request body: {error}"))
82 })?;
83
84 Ok(kernel.evaluate_plan_blocking(&parsed))
85}