1use std::collections::BTreeMap;
2
3use chio_core::capability::governance::GovernedTransactionIntent;
4use chio_core::{canonical_json_bytes, sha256_hex};
5use serde::{Deserialize, Serialize};
6use serde_json::{json, Value};
7
8use crate::discovery::{parse_discovery_protocol, DiscoveryProtocol, TargetProtocolRegistry};
9use crate::error::BridgeError;
10use crate::execution::TargetExecutionHop;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct CrossProtocolRouteEvidence {
17 pub selected_protocols: Vec<DiscoveryProtocol>,
18 pub terminal_protocol: DiscoveryProtocol,
19 pub multi_hop: bool,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(rename_all = "camelCase")]
25pub struct RouteAvailabilityStatus {
26 pub available: bool,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub reason: Option<String>,
29}
30
31impl RouteAvailabilityStatus {
32 #[must_use]
33 pub fn available() -> Self {
34 Self {
35 available: true,
36 reason: None,
37 }
38 }
39
40 #[must_use]
41 pub fn unavailable(reason: impl Into<String>) -> Self {
42 Self {
43 available: false,
44 reason: Some(reason.into()),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(rename_all = "camelCase")]
52pub struct RouteCandidateEvidence {
53 pub route_id: String,
54 pub target_protocol: DiscoveryProtocol,
55 pub selected_protocols: Vec<DiscoveryProtocol>,
56 pub available: bool,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub availability_reason: Option<String>,
59}
60
61#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "snake_case")]
64pub enum RouteSelectionDecision {
65 Select,
66 Attenuate,
67 Deny,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(rename_all = "camelCase")]
73pub struct RouteSelectionEvidence {
74 pub route_selection_id: String,
75 pub decision: RouteSelectionDecision,
76 pub source_protocol: DiscoveryProtocol,
77 pub requested_target_protocol: DiscoveryProtocol,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub selected_route_id: Option<String>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub selected_target_protocol: Option<DiscoveryProtocol>,
82 pub selected_protocols: Vec<DiscoveryProtocol>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub reason: Option<String>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub governed_intent_id: Option<String>,
87 pub candidates: Vec<RouteCandidateEvidence>,
88}
89
90#[derive(Debug, Clone)]
92pub struct RoutePlanningOutcome {
93 pub selected_target_protocol: Option<DiscoveryProtocol>,
94 pub evidence: RouteSelectionEvidence,
95}
96
97pub fn plan_authoritative_route(
99 request_id: &str,
100 source_protocol: DiscoveryProtocol,
101 requested_target_protocol: DiscoveryProtocol,
102 governed_intent: Option<&GovernedTransactionIntent>,
103 registry: &TargetProtocolRegistry<'_>,
104 availability: &BTreeMap<DiscoveryProtocol, RouteAvailabilityStatus>,
105) -> Result<RoutePlanningOutcome, BridgeError> {
106 let hints = route_planning_hints(governed_intent)?;
107 let mut targets = vec![requested_target_protocol];
108 if let Some(preferred) = hints.preferred_target_protocol {
109 if !targets.contains(&preferred) {
110 targets.push(preferred);
111 }
112 }
113 if hints.disallow_projected_protocols
114 && requested_target_protocol != DiscoveryProtocol::Native
115 && !targets.contains(&DiscoveryProtocol::Native)
116 {
117 targets.push(DiscoveryProtocol::Native);
118 }
119 if hints.allow_native_fallback && !targets.contains(&DiscoveryProtocol::Native) {
120 targets.push(DiscoveryProtocol::Native);
121 }
122
123 let candidates = targets
124 .into_iter()
125 .map(|target_protocol| {
126 build_route_candidate(source_protocol, target_protocol, registry, availability)
127 })
128 .collect::<Vec<_>>();
129
130 let available_candidate = |protocol: DiscoveryProtocol| -> Option<&RouteCandidateEvidence> {
131 candidates
132 .iter()
133 .find(|candidate| candidate.target_protocol == protocol && candidate.available)
134 };
135
136 let decision = if hints.disallow_projected_protocols
137 && requested_target_protocol != DiscoveryProtocol::Native
138 {
139 if let Some(candidate) = available_candidate(DiscoveryProtocol::Native) {
140 planned_outcome(
141 request_id,
142 RouteSelectionDecision::Attenuate,
143 source_protocol,
144 requested_target_protocol,
145 Some(candidate),
146 Some("governed intent disallowed projected protocols; selected native route"),
147 governed_intent,
148 &candidates,
149 )?
150 } else {
151 planned_outcome(
152 request_id,
153 RouteSelectionDecision::Deny,
154 source_protocol,
155 requested_target_protocol,
156 candidates.first(),
157 Some("governed intent disallowed projected protocols and no native route was available"),
158 governed_intent,
159 &candidates,
160 )?
161 }
162 } else if let Some(preferred) = hints.preferred_target_protocol {
163 if let Some(candidate) = available_candidate(preferred) {
164 planned_outcome(
165 request_id,
166 if preferred == requested_target_protocol {
167 RouteSelectionDecision::Select
168 } else {
169 RouteSelectionDecision::Attenuate
170 },
171 source_protocol,
172 requested_target_protocol,
173 Some(candidate),
174 if preferred == requested_target_protocol {
175 None
176 } else {
177 Some("control-plane policy preferred an alternate target protocol")
178 },
179 governed_intent,
180 &candidates,
181 )?
182 } else if let Some(candidate) = available_candidate(requested_target_protocol) {
183 planned_outcome(
184 request_id,
185 RouteSelectionDecision::Select,
186 source_protocol,
187 requested_target_protocol,
188 Some(candidate),
189 Some("preferred target protocol unavailable; retained requested route"),
190 governed_intent,
191 &candidates,
192 )?
193 } else if let Some(candidate) = available_candidate(DiscoveryProtocol::Native) {
194 planned_outcome(
195 request_id,
196 RouteSelectionDecision::Attenuate,
197 source_protocol,
198 requested_target_protocol,
199 Some(candidate),
200 Some("preferred target protocol unavailable; attenuated to native fallback"),
201 governed_intent,
202 &candidates,
203 )?
204 } else {
205 planned_outcome(
206 request_id,
207 RouteSelectionDecision::Deny,
208 source_protocol,
209 requested_target_protocol,
210 candidates.first(),
211 Some("no candidate route satisfied the preferred target protocol policy"),
212 governed_intent,
213 &candidates,
214 )?
215 }
216 } else if let Some(candidate) = available_candidate(requested_target_protocol) {
217 planned_outcome(
218 request_id,
219 RouteSelectionDecision::Select,
220 source_protocol,
221 requested_target_protocol,
222 Some(candidate),
223 None,
224 governed_intent,
225 &candidates,
226 )?
227 } else if let Some(candidate) = available_candidate(DiscoveryProtocol::Native) {
228 planned_outcome(
229 request_id,
230 RouteSelectionDecision::Attenuate,
231 source_protocol,
232 requested_target_protocol,
233 Some(candidate),
234 Some("requested target protocol unavailable; attenuated to native fallback"),
235 governed_intent,
236 &candidates,
237 )?
238 } else {
239 planned_outcome(
240 request_id,
241 RouteSelectionDecision::Deny,
242 source_protocol,
243 requested_target_protocol,
244 candidates.first(),
245 Some("no candidate route was available at planning time"),
246 governed_intent,
247 &candidates,
248 )?
249 };
250
251 Ok(decision)
252}
253
254pub fn route_selection_metadata(evidence: &RouteSelectionEvidence) -> Result<Value, BridgeError> {
256 Ok(json!({
257 "route_selection": serde_json::to_value(evidence)
258 .map_err(|error| BridgeError::InvalidRequest(error.to_string()))?,
259 }))
260}
261
262#[derive(Debug, Default)]
263struct RoutePlanningHints {
264 preferred_target_protocol: Option<DiscoveryProtocol>,
265 allow_native_fallback: bool,
266 disallow_projected_protocols: bool,
267}
268
269fn route_planning_hints(
270 governed_intent: Option<&GovernedTransactionIntent>,
271) -> Result<RoutePlanningHints, BridgeError> {
272 let Some(context) = governed_intent.and_then(|intent| intent.context.as_ref()) else {
273 return Ok(RoutePlanningHints::default());
274 };
275 let Some(control_plane) = context
276 .get("chioControlPlane")
277 .or_else(|| context.get("chio_control_plane"))
278 else {
279 return Ok(RoutePlanningHints::default());
280 };
281 let Some(object) = control_plane.as_object() else {
282 return Err(BridgeError::InvalidRequest(
283 "governed intent arcControlPlane context must be an object".to_string(),
284 ));
285 };
286
287 let preferred_target_protocol = object
288 .get("preferredTargetProtocol")
289 .or_else(|| object.get("preferred_target_protocol"))
290 .and_then(Value::as_str)
291 .map(parse_discovery_protocol)
292 .transpose()
293 .map_err(BridgeError::InvalidRequest)?;
294
295 Ok(RoutePlanningHints {
296 preferred_target_protocol,
297 allow_native_fallback: object
298 .get("allowNativeFallback")
299 .or_else(|| object.get("allow_native_fallback"))
300 .and_then(Value::as_bool)
301 .unwrap_or(false),
302 disallow_projected_protocols: object
303 .get("disallowProjectedProtocols")
304 .or_else(|| object.get("disallow_projected_protocols"))
305 .and_then(Value::as_bool)
306 .unwrap_or(false),
307 })
308}
309
310fn build_route_candidate(
311 source_protocol: DiscoveryProtocol,
312 target_protocol: DiscoveryProtocol,
313 registry: &TargetProtocolRegistry<'_>,
314 availability: &BTreeMap<DiscoveryProtocol, RouteAvailabilityStatus>,
315) -> RouteCandidateEvidence {
316 let availability = if registry.supports_target_protocol(target_protocol) {
317 availability
318 .get(&target_protocol)
319 .cloned()
320 .unwrap_or_else(RouteAvailabilityStatus::available)
321 } else {
322 RouteAvailabilityStatus::unavailable(format!(
323 "target protocol `{target_protocol}` is not registered"
324 ))
325 };
326
327 RouteCandidateEvidence {
328 route_id: format!("{}-route", target_protocol.as_str()),
329 target_protocol,
330 selected_protocols: planned_protocols_for_target(source_protocol, target_protocol),
331 available: availability.available,
332 availability_reason: availability.reason,
333 }
334}
335
336fn planned_protocols_for_target(
337 source_protocol: DiscoveryProtocol,
338 target_protocol: DiscoveryProtocol,
339) -> Vec<DiscoveryProtocol> {
340 match target_protocol {
341 DiscoveryProtocol::Native => vec![source_protocol, DiscoveryProtocol::Native],
342 DiscoveryProtocol::Mcp | DiscoveryProtocol::OpenAi => {
343 vec![source_protocol, target_protocol, DiscoveryProtocol::Native]
344 }
345 _ => vec![source_protocol, target_protocol],
346 }
347}
348
349#[allow(clippy::too_many_arguments)]
350fn planned_outcome(
351 request_id: &str,
352 decision: RouteSelectionDecision,
353 source_protocol: DiscoveryProtocol,
354 requested_target_protocol: DiscoveryProtocol,
355 selected_candidate: Option<&RouteCandidateEvidence>,
356 reason: Option<&str>,
357 governed_intent: Option<&GovernedTransactionIntent>,
358 candidates: &[RouteCandidateEvidence],
359) -> Result<RoutePlanningOutcome, BridgeError> {
360 let selected_route_id = if decision == RouteSelectionDecision::Deny {
361 None
362 } else {
363 selected_candidate.map(|candidate| candidate.route_id.clone())
364 };
365 let selected_target_protocol = if decision == RouteSelectionDecision::Deny {
366 None
367 } else {
368 selected_candidate.map(|candidate| candidate.target_protocol)
369 };
370 let selected_protocols = selected_candidate
371 .map(|candidate| candidate.selected_protocols.clone())
372 .unwrap_or_else(|| {
373 candidates
374 .first()
375 .map(|candidate| candidate.selected_protocols.clone())
376 .unwrap_or_else(|| vec![source_protocol, requested_target_protocol])
377 });
378 let route_selection_id = sha256_hex(
379 &canonical_json_bytes(&json!({
380 "requestId": request_id,
381 "sourceProtocol": source_protocol,
382 "requestedTargetProtocol": requested_target_protocol,
383 "selectedRouteId": selected_route_id,
384 "selectedTargetProtocol": selected_target_protocol,
385 "selectedProtocols": selected_protocols,
386 "decision": decision,
387 "governedIntentId": governed_intent.map(|intent| intent.id.clone()),
388 }))
389 .map_err(|error| BridgeError::Canonical(error.to_string()))?,
390 );
391
392 Ok(RoutePlanningOutcome {
393 selected_target_protocol,
394 evidence: RouteSelectionEvidence {
395 route_selection_id,
396 decision,
397 source_protocol,
398 requested_target_protocol,
399 selected_route_id,
400 selected_target_protocol,
401 selected_protocols,
402 reason: reason.map(str::to_string),
403 governed_intent_id: governed_intent.map(|intent| intent.id.clone()),
404 candidates: candidates.to_vec(),
405 },
406 })
407}
408
409pub(crate) fn route_hops_from_planning(
410 evidence: &RouteSelectionEvidence,
411 kernel_request_id: &str,
412 receipt_id: &str,
413) -> Vec<TargetExecutionHop> {
414 let target_protocols = evidence
415 .selected_protocols
416 .iter()
417 .copied()
418 .skip(1)
419 .collect::<Vec<_>>();
420 let last_index = target_protocols.len().saturating_sub(1);
421
422 target_protocols
423 .into_iter()
424 .enumerate()
425 .map(|(index, protocol)| TargetExecutionHop {
426 protocol,
427 request_id: if index == 0 && protocol != DiscoveryProtocol::Native {
428 format!("{}:{}", kernel_request_id, protocol.as_str())
429 } else {
430 kernel_request_id.to_string()
431 },
432 receipt_id: (index == last_index).then(|| receipt_id.to_string()),
433 })
434 .collect()
435}
436
437pub(crate) fn build_route_evidence(
438 source_protocol: DiscoveryProtocol,
439 route_hops: &[TargetExecutionHop],
440) -> Result<CrossProtocolRouteEvidence, BridgeError> {
441 let Some(last_hop) = route_hops.last() else {
442 return Err(BridgeError::InvalidRequest(
443 "target executor must return at least one target-side hop".to_string(),
444 ));
445 };
446
447 Ok(CrossProtocolRouteEvidence {
448 selected_protocols: std::iter::once(source_protocol)
449 .chain(route_hops.iter().map(|hop| hop.protocol))
450 .collect(),
451 terminal_protocol: last_hop.protocol,
452 multi_hop: route_hops.len() > 1,
453 })
454}