Skip to main content

app_facade_api/
routing.rs

1//! Transport-neutral routing policy for public `/app/*` operations.
2//!
3//! This module describes where an App Facade operation is authoritative. It
4//! deliberately does not describe sockets, connection fallback, authentication,
5//! or Lion's legacy controller routing. Route data is generated from
6//! `manifest/routing.v1.json`, the language-neutral source of truth.
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ExecutionSelector {
10    /// Execute against the cloud control plane.
11    Cloud,
12    /// Follow the active Space mode: cloud Space or its selected Hub.
13    ScopeMode,
14    /// Execute on the selected Space Hub, independent of Space mode.
15    Hub,
16    /// Read `input.placement` from the canonical [`crate::FacadeCall`].
17    RequestPlacement,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum RelayPolicy {
22    /// The client must reach the selected executor directly.
23    DirectOnly,
24    /// A Hub-owned request may use the cloud connection as a transport relay.
25    CloudRelayAllowed,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct RoutePolicy {
30    pub execution: ExecutionSelector,
31    pub relay: RelayPolicy,
32}
33
34const DIRECT_CLOUD: RoutePolicy = RoutePolicy {
35    execution: ExecutionSelector::Cloud,
36    relay: RelayPolicy::DirectOnly,
37};
38const DIRECT_SCOPE: RoutePolicy = RoutePolicy {
39    execution: ExecutionSelector::ScopeMode,
40    relay: RelayPolicy::DirectOnly,
41};
42const DIRECT_HUB: RoutePolicy = RoutePolicy {
43    execution: ExecutionSelector::Hub,
44    relay: RelayPolicy::DirectOnly,
45};
46const RELAYABLE_HUB: RoutePolicy = RoutePolicy {
47    execution: ExecutionSelector::Hub,
48    relay: RelayPolicy::CloudRelayAllowed,
49};
50const REQUEST_PLACEMENT: RoutePolicy = RoutePolicy {
51    execution: ExecutionSelector::RequestPlacement,
52    relay: RelayPolicy::DirectOnly,
53};
54
55include!("routing.generated.rs");
56
57/// Resolve routing metadata for a canonical App Facade target.
58///
59/// Exact exceptions are evaluated before domain prefixes. Unknown `/app/*`
60/// targets intentionally receive the normal Space-mode policy; protocol
61/// support remains the receiving facade's responsibility.
62pub fn route_policy_for_target(target: &str) -> Option<RoutePolicy> {
63    if !target.starts_with(TARGET_PREFIX) {
64        return None;
65    }
66
67    // Every exact exception is resolved before any domain prefix.
68    if SCOPE_MODE_TARGETS.contains(&target) {
69        return Some(DIRECT_SCOPE);
70    }
71    if CLOUD_TARGETS.contains(&target) {
72        return Some(DIRECT_CLOUD);
73    }
74    if CLOUD_RELAY_TARGETS.contains(&target) {
75        return Some(RELAYABLE_HUB);
76    }
77
78    if REQUEST_PLACEMENT_PREFIXES
79        .iter()
80        .any(|prefix| target.starts_with(prefix))
81    {
82        return Some(REQUEST_PLACEMENT);
83    }
84    if HUB_PREFIXES.iter().any(|prefix| target.starts_with(prefix)) {
85        return Some(DIRECT_HUB);
86    }
87    if CLOUD_PREFIXES
88        .iter()
89        .any(|prefix| target.starts_with(prefix))
90    {
91        return Some(DIRECT_CLOUD);
92    }
93
94    Some(DIRECT_SCOPE)
95}
96
97/// Read request-controlled placement from the strict App Facade envelope.
98/// Domain payloads without the canonical `input` boundary are rejected.
99pub fn request_placement(payload: &str) -> Option<crate::messaging::Placement> {
100    let call: crate::FacadeCall = serde_json::from_str(payload).ok()?;
101    serde_json::from_value(call.input.get("placement")?.clone()).ok()
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn exact_exceptions_win_over_domain_defaults() {
110        assert_eq!(
111            route_policy_for_target("/app/scope/context/get"),
112            Some(DIRECT_SCOPE)
113        );
114        assert_eq!(
115            route_policy_for_target(crate::plugin::ENABLED_LIST_TARGET),
116            Some(RELAYABLE_HUB)
117        );
118        assert_eq!(
119            route_policy_for_target(crate::system::INSTANCES_TARGET),
120            Some(RELAYABLE_HUB)
121        );
122        assert_eq!(
123            route_policy_for_target(crate::plugin::ENABLE_START_TARGET),
124            Some(DIRECT_HUB)
125        );
126    }
127
128    #[test]
129    fn domains_select_their_authoritative_executor() {
130        assert_eq!(
131            route_policy_for_target("/app/scope/member/list"),
132            Some(DIRECT_CLOUD)
133        );
134        assert_eq!(
135            route_policy_for_target("/app/messaging/provider/list"),
136            Some(REQUEST_PLACEMENT)
137        );
138        assert_eq!(
139            route_policy_for_target("/app/object-storage/session/acquire"),
140            Some(DIRECT_HUB)
141        );
142        assert_eq!(
143            route_policy_for_target("/app/runtime/status/list"),
144            Some(DIRECT_HUB)
145        );
146        assert_eq!(
147            route_policy_for_target(crate::system::INSTANCES_TARGET),
148            Some(RELAYABLE_HUB)
149        );
150        assert_eq!(
151            route_policy_for_target(crate::system::HOSTS_TARGET),
152            Some(RELAYABLE_HUB)
153        );
154        assert_eq!(
155            route_policy_for_target(crate::system::HOSTS_RUNTIME_TARGET),
156            Some(RELAYABLE_HUB)
157        );
158        assert_eq!(
159            route_policy_for_target(crate::system::HOSTS_CLAIM_TARGET),
160            Some(RELAYABLE_HUB)
161        );
162        assert_eq!(
163            route_policy_for_target("/app/device/list"),
164            Some(DIRECT_SCOPE)
165        );
166    }
167
168    #[test]
169    fn legacy_targets_are_outside_the_contract() {
170        assert_eq!(route_policy_for_target("/scope/get"), None);
171        assert_eq!(route_policy_for_target("/chat"), None);
172    }
173
174    #[test]
175    fn placement_is_read_only_from_the_canonical_input() {
176        assert_eq!(
177            request_placement(r#"{"control":{"mode":"direct"},"input":{"placement":"local"}}"#),
178            Some(crate::messaging::Placement::Local)
179        );
180        assert_eq!(request_placement(r#"{"placement":"cloud"}"#), None);
181        assert_eq!(
182            request_placement(r#"{"control":{"mode":"direct"},"input":{}}"#),
183            None
184        );
185    }
186}