1use nucleus_substrate_core::Projection;
49use serde::{Deserialize, Serialize};
50
51pub const FLOW_BODY_VERSION: u32 = 1;
52
53pub const CONF_LEVELS: &[&str] = &["public", "internal", "secret"];
56pub const INTEG_LEVELS: &[&str] = &["adversarial", "untrusted", "trusted"];
57pub const AUTHORITY_LEVELS: &[&str] = &[
58 "no_authority",
59 "informational",
60 "suggestive",
61 "directive",
62];
63pub const TAINT_LEVELS: &[&str] = &["clean", "user_derived", "ai_generated"];
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67pub struct FlowBody {
68 pub version: u32,
69 pub node_count: usize,
70 pub session_confidentiality_ceiling: String,
72 pub session_integrity_ceiling: String,
75 pub session_authority_ceiling: String,
77 pub session_taint_ceiling: String,
79 pub has_adversarial_bid: bool,
83 pub has_ai_derived: bool,
85 pub has_confidential_data: bool,
88}
89
90#[allow(clippy::too_many_arguments)]
92pub fn flow_projection(
93 node_count: usize,
94 session_confidentiality_ceiling: impl Into<String>,
95 session_integrity_ceiling: impl Into<String>,
96 session_authority_ceiling: impl Into<String>,
97 session_taint_ceiling: impl Into<String>,
98 has_adversarial_bid: bool,
99 has_ai_derived: bool,
100 has_confidential_data: bool,
101) -> Projection {
102 let body = FlowBody {
103 version: FLOW_BODY_VERSION,
104 node_count,
105 session_confidentiality_ceiling: session_confidentiality_ceiling.into(),
106 session_integrity_ceiling: session_integrity_ceiling.into(),
107 session_authority_ceiling: session_authority_ceiling.into(),
108 session_taint_ceiling: session_taint_ceiling.into(),
109 has_adversarial_bid,
110 has_ai_derived,
111 has_confidential_data,
112 };
113 Projection::Flow(serde_json::to_value(body).expect("FlowBody serializes"))
114}
115
116pub fn verify_flow_projection_shape(body: &FlowBody) -> Result<(), FlowVerifyError> {
125 if body.version != FLOW_BODY_VERSION {
126 return Err(FlowVerifyError::UnsupportedBodyVersion(body.version));
127 }
128 if !CONF_LEVELS.contains(&body.session_confidentiality_ceiling.as_str()) {
129 return Err(FlowVerifyError::UnknownLevel {
130 axis: "confidentiality",
131 value: body.session_confidentiality_ceiling.clone(),
132 });
133 }
134 if !INTEG_LEVELS.contains(&body.session_integrity_ceiling.as_str()) {
135 return Err(FlowVerifyError::UnknownLevel {
136 axis: "integrity",
137 value: body.session_integrity_ceiling.clone(),
138 });
139 }
140 if !AUTHORITY_LEVELS.contains(&body.session_authority_ceiling.as_str()) {
141 return Err(FlowVerifyError::UnknownLevel {
142 axis: "authority",
143 value: body.session_authority_ceiling.clone(),
144 });
145 }
146 if !TAINT_LEVELS.contains(&body.session_taint_ceiling.as_str()) {
147 return Err(FlowVerifyError::UnknownLevel {
148 axis: "taint",
149 value: body.session_taint_ceiling.clone(),
150 });
151 }
152 if body.has_adversarial_bid && body.session_integrity_ceiling != "adversarial" {
157 return Err(FlowVerifyError::CeilingInconsistent {
158 flag: "has_adversarial_bid",
159 level: "integrity",
160 actual: body.session_integrity_ceiling.clone(),
161 });
162 }
163 if body.session_integrity_ceiling == "trusted" && body.has_adversarial_bid {
165 return Err(FlowVerifyError::CeilingInconsistent {
166 flag: "has_adversarial_bid",
167 level: "integrity",
168 actual: body.session_integrity_ceiling.clone(),
169 });
170 }
171 Ok(())
172}
173
174#[derive(Debug, thiserror::Error)]
175pub enum FlowVerifyError {
176 #[error("flow body version {0} not supported by this lifter")]
177 UnsupportedBodyVersion(u32),
178 #[error("unknown {axis} level: {value}")]
179 UnknownLevel {
180 axis: &'static str,
181 value: String,
182 },
183 #[error("flag `{flag}` is inconsistent with {level} ceiling = {actual}")]
184 CeilingInconsistent {
185 flag: &'static str,
186 level: &'static str,
187 actual: String,
188 },
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 fn happy_body() -> FlowBody {
196 FlowBody {
197 version: FLOW_BODY_VERSION,
198 node_count: 4,
199 session_confidentiality_ceiling: "internal".into(),
200 session_integrity_ceiling: "trusted".into(),
201 session_authority_ceiling: "informational".into(),
202 session_taint_ceiling: "user_derived".into(),
203 has_adversarial_bid: false,
204 has_ai_derived: false,
205 has_confidential_data: true,
206 }
207 }
208
209 #[test]
210 fn happy_body_verifies() {
211 let body = happy_body();
212 verify_flow_projection_shape(&body).expect("happy body verifies");
213 }
214
215 #[test]
216 fn unknown_integrity_level_rejected() {
217 let mut body = happy_body();
218 body.session_integrity_ceiling = "questionable".into();
219 let err = verify_flow_projection_shape(&body).unwrap_err();
220 assert!(matches!(err, FlowVerifyError::UnknownLevel { axis: "integrity", .. }));
221 }
222
223 #[test]
224 fn adversarial_flag_with_trusted_ceiling_rejected() {
225 let mut body = happy_body();
226 body.has_adversarial_bid = true;
228 let err = verify_flow_projection_shape(&body).unwrap_err();
229 assert!(matches!(
230 err,
231 FlowVerifyError::CeilingInconsistent {
232 flag: "has_adversarial_bid",
233 ..
234 }
235 ));
236 }
237
238 #[test]
239 fn adversarial_flag_with_adversarial_ceiling_accepted() {
240 let mut body = happy_body();
242 body.has_adversarial_bid = true;
243 body.session_integrity_ceiling = "adversarial".into();
244 verify_flow_projection_shape(&body).expect("g8 path");
245 }
246
247 #[test]
248 fn body_version_mismatch_rejected() {
249 let mut body = happy_body();
250 body.version = 99;
251 let err = verify_flow_projection_shape(&body).unwrap_err();
252 assert!(matches!(err, FlowVerifyError::UnsupportedBodyVersion(99)));
253 }
254
255 #[test]
256 fn flow_projection_helper_packs_correct_wire_shape() {
257 let projection = flow_projection(
258 7,
259 "internal",
260 "adversarial",
261 "informational",
262 "ai_generated",
263 true,
264 true,
265 true,
266 );
267 assert_eq!(projection.kind(), "flow");
268 let v = serde_json::to_value(&projection).unwrap();
269 assert_eq!(v["kind"], "flow");
270 assert_eq!(v["body"]["node_count"], 7);
271 assert_eq!(v["body"]["session_integrity_ceiling"], "adversarial");
272 assert_eq!(v["body"]["has_adversarial_bid"], true);
273 assert_eq!(v["body"]["has_ai_derived"], true);
274 assert_eq!(v["body"]["version"], FLOW_BODY_VERSION);
275 }
276}