Skip to main content

sim_lib_stream_host/
watch.rs

1//! Provider-side watch consent gates for worn stream expressions.
2
3use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
4use sim_value::access::field;
5
6/// Capability required for watch health and biometric streams.
7pub const CAP_WATCH_HEALTH: &str = "watch/health";
8
9/// Capability required for watch location and route streams.
10pub const CAP_WATCH_LOCATION: &str = "watch/location";
11
12/// Capability required for watch microphone audio.
13pub const CAP_WATCH_MIC: &str = "watch/mic";
14
15/// Capability required for vendor diagnostic reports.
16pub const CAP_WATCH_VENDOR_REPORT: &str = "watch/vendor-report";
17
18const WORN_SENSOR_NAMESPACE: &str = "stream/worn-sensor";
19const WORN_SAMPLE_NAMESPACE: &str = "stream/device-sample";
20const WORN_SAMPLE_KIND: &str = "worn-event";
21
22/// Watch-sensitive provider capability classes.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum WatchCapability {
25    /// Health and biometric worn streams.
26    Health,
27    /// GPS and route worn streams.
28    Location,
29    /// Raw microphone audio.
30    Mic,
31    /// Vendor diagnostics, off unless explicitly granted.
32    VendorReport,
33}
34
35impl WatchCapability {
36    /// All provider-side watch capabilities.
37    pub const ALL: [Self; 4] = [Self::Health, Self::Location, Self::Mic, Self::VendorReport];
38
39    /// Stable kernel capability name.
40    pub fn as_str(self) -> &'static str {
41        match self {
42            Self::Health => CAP_WATCH_HEALTH,
43            Self::Location => CAP_WATCH_LOCATION,
44            Self::Mic => CAP_WATCH_MIC,
45            Self::VendorReport => CAP_WATCH_VENDOR_REPORT,
46        }
47    }
48
49    /// Stable local token after the `watch/` prefix.
50    pub fn local_name(self) -> &'static str {
51        match self {
52            Self::Health => "health",
53            Self::Location => "location",
54            Self::Mic => "mic",
55            Self::VendorReport => "vendor-report",
56        }
57    }
58
59    /// Kernel capability value.
60    pub fn capability_name(self) -> CapabilityName {
61        CapabilityName::new(self.as_str())
62    }
63
64    /// Visible consent grant symbol.
65    pub fn grant_symbol(self) -> Symbol {
66        Symbol::qualified("watch", self.local_name())
67    }
68
69    /// Resolves a watch capability name.
70    pub fn from_name(name: &str) -> Option<Self> {
71        Self::ALL
72            .into_iter()
73            .find(|capability| capability.as_str() == name)
74    }
75}
76
77/// Returns the visible grant symbol for watch health streams.
78pub fn watch_health_grant() -> Symbol {
79    WatchCapability::Health.grant_symbol()
80}
81
82/// Returns the visible grant symbol for watch location streams.
83pub fn watch_location_grant() -> Symbol {
84    WatchCapability::Location.grant_symbol()
85}
86
87/// Returns the visible grant symbol for watch microphone input.
88pub fn watch_mic_grant() -> Symbol {
89    WatchCapability::Mic.grant_symbol()
90}
91
92/// Returns the visible grant symbol for watch vendor diagnostics.
93pub fn watch_vendor_report_grant() -> Symbol {
94    WatchCapability::VendorReport.grant_symbol()
95}
96
97/// Classifies a worn-event expression into the watch capability it needs.
98pub fn watch_capability_for_worn_event(event: &Expr) -> Result<WatchCapability> {
99    ensure_worn_event_sample(event)?;
100    let sensor = required_symbol_field(event, "sensor")?;
101    if sensor.namespace.as_deref() != Some(WORN_SENSOR_NAMESPACE) {
102        return Err(Error::HostError(format!(
103            "watch worn sensor must be in {WORN_SENSOR_NAMESPACE}, found {sensor}"
104        )));
105    }
106    Ok(match sensor.name.as_ref() {
107        "gps" | "route" => WatchCapability::Location,
108        "mic-audio" => WatchCapability::Mic,
109        _ => WatchCapability::Health,
110    })
111}
112
113/// Requires kernel authority, visible consent, and same-session receipt ownership.
114pub fn require_watch_consent(
115    cx: &Cx,
116    capability: WatchCapability,
117    grants: &[Symbol],
118    receipt_session: &Symbol,
119    session: &Symbol,
120) -> Result<()> {
121    cx.require(&capability.capability_name())?;
122    if receipt_session != session {
123        return Err(Error::HostError(format!(
124            "{}: consent not for this session",
125            capability.as_str()
126        )));
127    }
128    if !grants
129        .iter()
130        .any(|grant| grant.as_qualified_str() == capability.as_str())
131    {
132        return Err(Error::HostError(format!(
133            "{} requires visible consent",
134            capability.as_str()
135        )));
136    }
137    Ok(())
138}
139
140/// Requires the authority needed to ingest one worn-event expression.
141pub fn require_watch_worn_ingest(
142    cx: &Cx,
143    event: &Expr,
144    grants: &[Symbol],
145    receipt_session: &Symbol,
146    session: &Symbol,
147) -> Result<WatchCapability> {
148    let capability = watch_capability_for_worn_event(event)?;
149    require_watch_consent(cx, capability, grants, receipt_session, session)?;
150    Ok(capability)
151}
152
153fn ensure_worn_event_sample(event: &Expr) -> Result<()> {
154    let sample = required_symbol_field(event, "sample")?;
155    if sample.namespace.as_deref() == Some(WORN_SAMPLE_NAMESPACE)
156        && sample.name.as_ref() == WORN_SAMPLE_KIND
157    {
158        Ok(())
159    } else {
160        Err(Error::HostError(
161            "expected stream/device-sample worn-event".to_owned(),
162        ))
163    }
164}
165
166fn required_symbol_field(expr: &Expr, name: &str) -> Result<Symbol> {
167    match field(expr, name) {
168        Some(Expr::Symbol(symbol)) => Ok(symbol.clone()),
169        Some(_) => Err(Error::TypeMismatch {
170            expected: "symbol field",
171            found: "non-symbol",
172        }),
173        None => Err(Error::Eval(format!("missing field {name}"))),
174    }
175}