1use 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
11pub const CAP_GLASSES_POSE: &str = "glasses/pose";
13
14pub const CAP_GLASSES_CAMERA: &str = "glasses/camera";
16
17pub const CAP_GLASSES_WORLD_ANCHOR: &str = "glasses/world-anchor";
19
20pub const CAP_GLASSES_HAND: &str = "glasses/hand";
22
23pub const CAP_GLASSES_MIC: &str = "glasses/mic";
25
26pub const CAP_GLASSES_VENDOR_REPORT: &str = "glasses/vendor-report";
28
29const XR_NAMESPACE: &str = "xr";
30const GLASSES_NAMESPACE: &str = "glasses";
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum GlassesCapability {
35 Pose,
37 Camera,
39 WorldAnchor,
41 Hand,
43 Mic,
45 VendorReport,
47}
48
49impl GlassesCapability {
50 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 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 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 pub fn capability_name(self) -> CapabilityName {
86 CapabilityName::new(self.as_str())
87 }
88
89 pub fn grant_symbol(self) -> Symbol {
91 Symbol::qualified(GLASSES_NAMESPACE, self.local_name())
92 }
93
94 pub fn from_name(name: &str) -> Option<Self> {
96 Self::ALL
97 .into_iter()
98 .find(|capability| capability.as_str() == name)
99 }
100}
101
102pub fn glasses_pose_grant() -> Symbol {
104 GlassesCapability::Pose.grant_symbol()
105}
106
107pub fn glasses_camera_grant() -> Symbol {
109 GlassesCapability::Camera.grant_symbol()
110}
111
112pub fn glasses_world_anchor_grant() -> Symbol {
114 GlassesCapability::WorldAnchor.grant_symbol()
115}
116
117pub fn glasses_hand_grant() -> Symbol {
119 GlassesCapability::Hand.grant_symbol()
120}
121
122pub fn glasses_mic_grant() -> Symbol {
124 GlassesCapability::Mic.grant_symbol()
125}
126
127pub fn glasses_vendor_report_grant() -> Symbol {
129 GlassesCapability::VendorReport.grant_symbol()
130}
131
132pub fn glasses_mic_capability() -> CapabilityName {
134 GlassesCapability::Mic.capability_name()
135}
136
137pub 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
159pub 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
169pub 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
181pub 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
211pub 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
220pub 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
240pub 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}