Skip to main content

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}