Skip to main content

car_parslee/
mobile_runtime.rs

1//! Parslee mobile runtime registration.
2//!
3//! A signed-in CAR daemon can advertise a mobile-reachable Parslee Core runtime
4//! to Parslee so the iOS/Android apps can discover the user's machine after
5//! account sign-in. Registration is best-effort: local daemon startup must not
6//! depend on Parslee cloud reachability.
7
8use serde::Serialize;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
11pub struct MobileRuntimeRegistration {
12    pub name: String,
13    pub source: String,
14    pub url: String,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub token: Option<String>,
17    pub capabilities: Vec<String>,
18}
19
20impl MobileRuntimeRegistration {
21    pub fn new(url: impl Into<String>, token: Option<String>) -> Self {
22        let url = url.into();
23        Self {
24            name: "Parslee Core".to_string(),
25            source: "car".to_string(),
26            url,
27            token,
28            capabilities: vec![
29                "chat".to_string(),
30                "approval".to_string(),
31                "a2ui".to_string(),
32                "notify_linked_device".to_string(),
33                "verified_goals".to_string(),
34            ],
35        }
36    }
37}
38
39pub async fn register(
40    api_base: &str,
41    bearer: &str,
42    registration: &MobileRuntimeRegistration,
43) -> Result<(), String> {
44    let client = reqwest::Client::builder()
45        .timeout(std::time::Duration::from_secs(4))
46        .connect_timeout(std::time::Duration::from_secs(2))
47        .redirect(reqwest::redirect::Policy::none())
48        .build()
49        .map_err(|e| format!("build Parslee mobile runtime client: {e}"))?;
50    let response = client
51        .post(endpoint(api_base))
52        .bearer_auth(bearer)
53        .json(registration)
54        .send()
55        .await
56        .map_err(|e| format!("register Parslee mobile runtime: {e}"))?;
57    let status = response.status();
58    if !status.is_success() {
59        let body = response.text().await.unwrap_or_default();
60        return Err(format!(
61            "register Parslee mobile runtime failed: HTTP {status}: {body}"
62        ));
63    }
64    Ok(())
65}
66
67pub fn endpoint(api_base: &str) -> String {
68    format!("{}/mobile/runtimes", api_base.trim_end_matches('/'))
69}
70
71pub fn configured_api_base() -> String {
72    car_auth::api_base(None)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn registration_body_matches_mobile_discovery_shape() {
81        let body = MobileRuntimeRegistration::new(
82            "wss://home.example/mobile?device=mac studio",
83            Some("pair+/=123".to_string()),
84        );
85
86        assert_eq!(body.name, "Parslee Core");
87        assert_eq!(body.source, "car");
88        assert_eq!(body.url, "wss://home.example/mobile?device=mac studio");
89        assert_eq!(body.token.as_deref(), Some("pair+/=123"));
90        assert!(body.capabilities.contains(&"verified_goals".to_string()));
91    }
92
93    #[test]
94    fn endpoint_uses_mobile_runtimes_contract() {
95        assert_eq!(
96            endpoint("https://api.parslee.ai/"),
97            "https://api.parslee.ai/mobile/runtimes"
98        );
99    }
100}