Skip to main content

chio_cross_protocol/
routing.rs

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/// Route evidence emitted by the shared fabric for an authoritative bridged
13/// execution.
14#[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/// Availability state for one route family at planning time.
23#[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/// Candidate route considered by the shared control plane.
50#[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/// Planner decision for a route candidate set.
62#[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/// Signed route-selection evidence emitted by the shared control plane.
71#[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/// Concrete planning result used by the shared orchestrator.
91#[derive(Debug, Clone)]
92pub struct RoutePlanningOutcome {
93    pub selected_target_protocol: Option<DiscoveryProtocol>,
94    pub evidence: RouteSelectionEvidence,
95}
96
97/// Plan a control-plane route selection for an authoritative call.
98pub 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
254/// Build receipt metadata wrapper for signed route-selection evidence.
255pub 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}