Skip to main content

chio_http_core/
routes.rs

1//! Route path constants shared across every HTTP substrate adapter.
2//!
3//! `chio-http-core` does not ship an HTTP server; it is the protocol-
4//! agnostic types crate that every substrate (`chio-tower`,
5//! `chio-api-protect`, hosted sidecars) builds on top of. Centralizing
6//! the route strings here keeps each adapter in sync with the spec in
7//! `docs/protocols/STRUCTURAL-SECURITY-FIXES.md` section 5.4.
8//!
9//! # Adapter wiring
10//!
11//! Every adapter is expected to:
12//!
13//! 1. Construct a single [`crate::emergency::EmergencyAdmin`] at
14//!    startup, bound to an `Arc<ChioKernel>` and the operator-configured
15//!    admin token.
16//! 2. Register three framework-native routes on the paths defined
17//!    here, pulling the token out of the [`EMERGENCY_ADMIN_TOKEN_HEADER`]
18//!    request header and delegating to the corresponding
19//!    `handle_emergency_*` function in [`crate::emergency`].
20//! 3. Map [`crate::emergency::EmergencyHandlerError`] status codes and
21//!    bodies onto the framework's response type without re-interpreting
22//!    the semantics. No adapter should add extra authentication on top
23//!    of `X-Admin-Token`; the handler already fails closed.
24//!
25//! The helper [`emergency_route_registrations`] returns a compact
26//! description of every registration an adapter must perform, so
27//! adapters can iterate over the triple instead of copying constants
28//! at each call site.
29
30use crate::method::HttpMethod;
31
32/// `POST /emergency-stop` -- engage the kernel kill switch.
33pub const EMERGENCY_STOP_PATH: &str = "/emergency-stop";
34
35/// `POST /emergency-resume` -- disengage the kill switch.
36pub const EMERGENCY_RESUME_PATH: &str = "/emergency-resume";
37
38/// `GET /emergency-status` -- report current kill-switch state.
39pub const EMERGENCY_STATUS_PATH: &str = "/emergency-status";
40
41/// `POST /evaluate-plan` -- Phase 2.4 plan-level pre-flight evaluation.
42pub const EVALUATE_PLAN_PATH: &str = "/evaluate-plan";
43
44/// `GET /approvals/pending` -- Phase 3.4-3.6 list of outstanding HITL
45/// approvals, optionally filtered by query parameters.
46pub const APPROVALS_PENDING_PATH: &str = "/approvals/pending";
47
48/// `GET /approvals/{id}` -- fetch one approval (pending or resolved).
49///
50/// The `{id}` placeholder is substituted by the adapter's routing
51/// layer; this constant is the route template string so adapters can
52/// pass it to their router as-is.
53pub const APPROVALS_GET_PATH: &str = "/approvals/{id}";
54
55/// `POST /approvals/{id}/respond` -- submit a decision for a single
56/// pending approval.
57pub const APPROVALS_RESPOND_PATH: &str = "/approvals/{id}/respond";
58
59/// `POST /approvals/batch/respond` -- submit decisions for multiple
60/// pending approvals in one request.
61pub const APPROVALS_BATCH_RESPOND_PATH: &str = "/approvals/batch/respond";
62
63/// Describe every approval route an adapter must expose.
64#[must_use]
65pub const fn approval_route_registrations() -> [EmergencyRouteRegistration; 4] {
66    [
67        EmergencyRouteRegistration {
68            method: HttpMethod::Get,
69            path: APPROVALS_PENDING_PATH,
70            name: "approvals_pending",
71        },
72        EmergencyRouteRegistration {
73            method: HttpMethod::Get,
74            path: APPROVALS_GET_PATH,
75            name: "approvals_get",
76        },
77        EmergencyRouteRegistration {
78            method: HttpMethod::Post,
79            path: APPROVALS_RESPOND_PATH,
80            name: "approvals_respond",
81        },
82        EmergencyRouteRegistration {
83            method: HttpMethod::Post,
84            path: APPROVALS_BATCH_RESPOND_PATH,
85            name: "approvals_batch_respond",
86        },
87    ]
88}
89
90/// Header that carries the operator admin token on every emergency
91/// call. Adapters must not expose these routes without requiring this
92/// header; see [`crate::emergency::EmergencyAdmin::new`].
93pub const EMERGENCY_ADMIN_TOKEN_HEADER: &str = "X-Admin-Token";
94
95/// Route descriptor used by [`emergency_route_registrations`].
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct EmergencyRouteRegistration {
98    /// HTTP method for this route.
99    pub method: HttpMethod,
100    /// URL path for this route.
101    pub path: &'static str,
102    /// Stable identifier adapters can use when emitting metrics.
103    pub name: &'static str,
104}
105
106/// Compact description of every route a substrate adapter must expose
107/// for the emergency kill switch. Returned as an array so adapters can
108/// iterate without heap allocation.
109#[must_use]
110pub const fn emergency_route_registrations() -> [EmergencyRouteRegistration; 3] {
111    [
112        EmergencyRouteRegistration {
113            method: HttpMethod::Post,
114            path: EMERGENCY_STOP_PATH,
115            name: "emergency_stop",
116        },
117        EmergencyRouteRegistration {
118            method: HttpMethod::Post,
119            path: EMERGENCY_RESUME_PATH,
120            name: "emergency_resume",
121        },
122        EmergencyRouteRegistration {
123            method: HttpMethod::Get,
124            path: EMERGENCY_STATUS_PATH,
125            name: "emergency_status",
126        },
127    ]
128}
129
130// ---------------------------------------------------------------------------
131// Phase 19.1 -- compliance score endpoint.
132// ---------------------------------------------------------------------------
133
134/// `POST /compliance/score` -- compute a 0..=1000 compliance score for
135/// an agent over a window. Substrate adapters delegate to
136/// [`crate::compliance::handle_compliance_score`].
137pub const COMPLIANCE_SCORE_PATH: &str = "/compliance/score";
138
139// ---------------------------------------------------------------------------
140// Phase 19.3 -- regulatory receipt export endpoint.
141// ---------------------------------------------------------------------------
142
143/// `GET /regulatory/receipts` -- read-only signed export of receipts
144/// for regulators. Every response is a
145/// [`crate::regulatory_api::SignedRegulatoryReceiptExport`].
146pub const REGULATORY_RECEIPTS_PATH: &str = "/regulatory/receipts";
147
148/// Header that carries the regulator token. Adapters must not expose
149/// `/regulatory/*` without requiring this header; the handler fails
150/// closed when the caller identity is missing.
151pub const REGULATORY_TOKEN_HEADER: &str = "X-Regulatory-Token";
152
153/// Route descriptors for the regulatory API. Appended here (do not
154/// reorder existing registrations) per the phase-19 bundle contract.
155#[must_use]
156pub const fn regulatory_route_registrations() -> [EmergencyRouteRegistration; 2] {
157    [
158        EmergencyRouteRegistration {
159            method: HttpMethod::Post,
160            path: COMPLIANCE_SCORE_PATH,
161            name: "compliance_score",
162        },
163        EmergencyRouteRegistration {
164            method: HttpMethod::Get,
165            path: REGULATORY_RECEIPTS_PATH,
166            name: "regulatory_receipts",
167        },
168    ]
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn emergency_route_constants_match_spec() {
177        assert_eq!(EMERGENCY_STOP_PATH, "/emergency-stop");
178        assert_eq!(EMERGENCY_RESUME_PATH, "/emergency-resume");
179        assert_eq!(EMERGENCY_STATUS_PATH, "/emergency-status");
180        assert_eq!(EMERGENCY_ADMIN_TOKEN_HEADER, "X-Admin-Token");
181    }
182
183    #[test]
184    fn regulatory_route_constants_match_spec() {
185        assert_eq!(COMPLIANCE_SCORE_PATH, "/compliance/score");
186        assert_eq!(REGULATORY_RECEIPTS_PATH, "/regulatory/receipts");
187        assert_eq!(REGULATORY_TOKEN_HEADER, "X-Regulatory-Token");
188
189        let registrations = regulatory_route_registrations();
190        assert_eq!(registrations.len(), 2);
191        let names: Vec<&str> = registrations.iter().map(|r| r.name).collect();
192        assert!(names.contains(&"compliance_score"));
193        assert!(names.contains(&"regulatory_receipts"));
194    }
195
196    #[test]
197    fn registrations_cover_all_three_endpoints() {
198        let registrations = emergency_route_registrations();
199        assert_eq!(registrations.len(), 3);
200        let names: Vec<&str> = registrations.iter().map(|r| r.name).collect();
201        assert!(names.contains(&"emergency_stop"));
202        assert!(names.contains(&"emergency_resume"));
203        assert!(names.contains(&"emergency_status"));
204
205        let stop = registrations.iter().find(|r| r.name == "emergency_stop");
206        assert!(
207            matches!(stop, Some(r) if matches!(r.method, HttpMethod::Post)),
208            "stop registration must exist and use POST"
209        );
210
211        let status = registrations.iter().find(|r| r.name == "emergency_status");
212        assert!(
213            matches!(status, Some(r) if matches!(r.method, HttpMethod::Get)),
214            "status registration must exist and use GET"
215        );
216    }
217}