Skip to main content

sim_lib_view_spatial/
consent.rs

1//! Glasses-specific consent gates over the shared device consent contract.
2
3use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
4use sim_lib_scene::{GlanceCard, GlanceMetric};
5use sim_lib_view_device::{
6    ConsentReceipt, DeviceSampleStore, EdgeId, Evicted, FrameClock, RetentionReaper, StoreKey,
7    StoredSample, require_with_consent,
8};
9use sim_value::{access, build};
10
11/// Capability required for glasses pose and tracking samples.
12pub const CAP_GLASSES_POSE: &str = "glasses/pose";
13
14/// Capability required for glasses camera frames.
15pub const CAP_GLASSES_CAMERA: &str = "glasses/camera";
16
17/// Capability required for stable world-anchor observations.
18pub const CAP_GLASSES_WORLD_ANCHOR: &str = "glasses/world-anchor";
19
20/// Capability required for glasses hand-ray samples.
21pub const CAP_GLASSES_HAND: &str = "glasses/hand";
22
23/// Capability required for glasses microphone capture.
24pub const CAP_GLASSES_MIC: &str = "glasses/mic";
25
26/// Capability required for vendor diagnostic reporting.
27pub const CAP_GLASSES_VENDOR_REPORT: &str = "glasses/vendor-report";
28
29const XR_NAMESPACE: &str = "xr";
30const GLASSES_NAMESPACE: &str = "glasses";
31
32/// Glasses-sensitive capability classes.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum GlassesCapability {
35    /// Pose or spatial tracking samples.
36    Pose,
37    /// Camera frame references.
38    Camera,
39    /// Stable world-anchor observations.
40    WorldAnchor,
41    /// Hand-ray samples.
42    Hand,
43    /// Raw microphone chunk references.
44    Mic,
45    /// Vendor diagnostics, off unless explicitly granted.
46    VendorReport,
47}
48
49impl GlassesCapability {
50    /// All glasses-sensitive capabilities.
51    pub const ALL: [Self; 6] = [
52        Self::Pose,
53        Self::Camera,
54        Self::WorldAnchor,
55        Self::Hand,
56        Self::Mic,
57        Self::VendorReport,
58    ];
59
60    /// Stable kernel capability name.
61    pub fn as_str(self) -> &'static str {
62        match self {
63            Self::Pose => CAP_GLASSES_POSE,
64            Self::Camera => CAP_GLASSES_CAMERA,
65            Self::WorldAnchor => CAP_GLASSES_WORLD_ANCHOR,
66            Self::Hand => CAP_GLASSES_HAND,
67            Self::Mic => CAP_GLASSES_MIC,
68            Self::VendorReport => CAP_GLASSES_VENDOR_REPORT,
69        }
70    }
71
72    /// Stable local token after the `glasses/` prefix.
73    pub fn local_name(self) -> &'static str {
74        match self {
75            Self::Pose => "pose",
76            Self::Camera => "camera",
77            Self::WorldAnchor => "world-anchor",
78            Self::Hand => "hand",
79            Self::Mic => "mic",
80            Self::VendorReport => "vendor-report",
81        }
82    }
83
84    /// Kernel capability value.
85    pub fn capability_name(self) -> CapabilityName {
86        CapabilityName::new(self.as_str())
87    }
88
89    /// Visible consent grant symbol carried by a [`ConsentReceipt`].
90    pub fn grant_symbol(self) -> Symbol {
91        Symbol::qualified(GLASSES_NAMESPACE, self.local_name())
92    }
93
94    /// Resolves a glasses capability name.
95    pub fn from_name(name: &str) -> Option<Self> {
96        Self::ALL
97            .into_iter()
98            .find(|capability| capability.as_str() == name)
99    }
100}
101
102/// Returns the visible grant symbol for glasses pose samples.
103pub fn glasses_pose_grant() -> Symbol {
104    GlassesCapability::Pose.grant_symbol()
105}
106
107/// Returns the visible grant symbol for glasses camera frames.
108pub fn glasses_camera_grant() -> Symbol {
109    GlassesCapability::Camera.grant_symbol()
110}
111
112/// Returns the visible grant symbol for glasses world-anchor observations.
113pub fn glasses_world_anchor_grant() -> Symbol {
114    GlassesCapability::WorldAnchor.grant_symbol()
115}
116
117/// Returns the visible grant symbol for glasses hand-ray samples.
118pub fn glasses_hand_grant() -> Symbol {
119    GlassesCapability::Hand.grant_symbol()
120}
121
122/// Returns the visible grant symbol for glasses microphone capture.
123pub fn glasses_mic_grant() -> Symbol {
124    GlassesCapability::Mic.grant_symbol()
125}
126
127/// Returns the visible grant symbol for glasses vendor diagnostics.
128pub fn glasses_vendor_report_grant() -> Symbol {
129    GlassesCapability::VendorReport.grant_symbol()
130}
131
132/// Returns the kernel capability name for glasses microphone capture.
133pub fn glasses_mic_capability() -> CapabilityName {
134    GlassesCapability::Mic.capability_name()
135}
136
137/// Classifies an expression into the glasses capability it needs.
138pub fn glasses_capability_for_expr(expr: &Expr) -> Result<GlassesCapability> {
139    if access::field(expr, "world-anchor").is_some() {
140        return Ok(GlassesCapability::WorldAnchor);
141    }
142    let symbol = access::field_sym(expr, "sample")
143        .or_else(|| access::field_sym(expr, "kind"))
144        .ok_or_else(|| Error::Eval("missing glasses sample or kind field".to_owned()))?;
145    match (symbol.namespace.as_deref(), symbol.name.as_ref()) {
146        (Some(XR_NAMESPACE), "pose") => Ok(GlassesCapability::Pose),
147        (Some(XR_NAMESPACE), "camera-frame") => Ok(GlassesCapability::Camera),
148        (Some(XR_NAMESPACE), "hand") => Ok(GlassesCapability::Hand),
149        (Some(XR_NAMESPACE), "mic-chunk") => Ok(GlassesCapability::Mic),
150        (Some(GLASSES_NAMESPACE), "world-anchor") => Ok(GlassesCapability::WorldAnchor),
151        (Some(GLASSES_NAMESPACE), "vendor-report") => Ok(GlassesCapability::VendorReport),
152        _ => Err(Error::HostError(format!(
153            "no glasses consent capability for {}",
154            symbol.as_qualified_str()
155        ))),
156    }
157}
158
159/// Requires the named glasses capability and a session-bound visible consent receipt.
160pub fn require_glasses_consent(
161    cx: &Cx,
162    capability: GlassesCapability,
163    receipt: &ConsentReceipt,
164    session: &EdgeId,
165) -> Result<()> {
166    require_with_consent(cx, capability.as_str(), receipt, session)
167}
168
169/// Requires the authority needed to consume one glasses-sensitive expression.
170pub fn require_glasses_expr_consent(
171    cx: &Cx,
172    expr: &Expr,
173    receipt: &ConsentReceipt,
174    session: &EdgeId,
175) -> Result<GlassesCapability> {
176    let capability = glasses_capability_for_expr(expr)?;
177    require_glasses_consent(cx, capability, receipt, session)?;
178    Ok(capability)
179}
180
181/// Stores one glasses-sensitive sample under the receipt sequence and modeled clock.
182pub fn store_glasses_sample(
183    store: &mut DeviceSampleStore,
184    capability: GlassesCapability,
185    sample_id: impl Into<String>,
186    value: Expr,
187    receipt: &ConsentReceipt,
188    clock: FrameClock,
189    content_refs: Vec<StoreKey>,
190) -> Result<StoreKey> {
191    let sample_id = sample_id.into();
192    if sample_id.is_empty() {
193        return Err(Error::Eval(
194            "glasses sample id must not be empty".to_owned(),
195        ));
196    }
197    let key = StoreKey::new(Symbol::qualified(
198        "glasses/store",
199        format!("{}-{sample_id}", capability.local_name()),
200    ));
201    store.insert_sample(StoredSample::new(
202        key.clone(),
203        receipt.seq,
204        clock.tick,
205        content_refs,
206        stored_glasses_value(capability, value),
207    ));
208    Ok(key)
209}
210
211/// Runs the shared retention reaper for glasses-sensitive samples.
212pub fn sweep_glasses_privacy(
213    store: &mut DeviceSampleStore,
214    receipts: &[ConsentReceipt],
215    clock: FrameClock,
216) -> Vec<Evicted> {
217    RetentionReaper::new().sweep(store, receipts, clock)
218}
219
220/// Renders active glasses grants and retention windows for the rich surface.
221pub fn active_glasses_consent_badge_cluster(receipts: &[ConsentReceipt]) -> Expr {
222    let mut badges = Vec::new();
223    for receipt in receipts {
224        for grant in glasses_grants(receipt) {
225            badges.push(sim_lib_scene::badge(
226                "ok",
227                &format!("{} {}ms", grant.as_qualified_str(), receipt.retain_ms),
228            ));
229        }
230        if !receipt.redact.is_empty() && receipt_has_glasses_grants(receipt) {
231            badges.push(sim_lib_scene::badge(
232                "warn",
233                &format!("retention {}ms", receipt.retain_ms),
234            ));
235        }
236    }
237    sim_lib_scene::badge_cluster(badges)
238}
239
240/// Renders active glasses consent as one compact Halo glance card.
241pub fn halo_consent_glyph(receipts: &[ConsentReceipt]) -> Expr {
242    let grant_count = receipts.iter().flat_map(glasses_grants).count();
243    let retain_ms = receipts
244        .iter()
245        .filter(|receipt| receipt_has_glasses_grants(receipt))
246        .map(|receipt| receipt.retain_ms)
247        .min()
248        .unwrap_or(0);
249    GlanceCard::new(
250        "Consent",
251        Some(GlanceMetric::new(
252            "grants",
253            format!("{grant_count}/{retain_ms}ms"),
254        )),
255        None,
256        "info",
257        1,
258    )
259    .to_scene()
260}
261
262fn stored_glasses_value(capability: GlassesCapability, value: Expr) -> Expr {
263    build::map(vec![
264        ("kind", build::qsym("glasses", "sensitive-sample")),
265        ("capability", Expr::Symbol(capability.grant_symbol())),
266        (capability.local_name(), value),
267    ])
268}
269
270fn glasses_grants(receipt: &ConsentReceipt) -> impl Iterator<Item = &Symbol> {
271    receipt
272        .grants
273        .iter()
274        .filter(|grant| grant.namespace.as_deref() == Some(GLASSES_NAMESPACE))
275}
276
277fn receipt_has_glasses_grants(receipt: &ConsentReceipt) -> bool {
278    glasses_grants(receipt).next().is_some()
279}