1use sim_kernel::{CapabilityName, Cx, Error, EventKind, EventLedger, Expr, Ref, Result, Symbol};
4use sim_lib_scene::{GlanceCard, GlanceMetric};
5use sim_value::{access, build};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum DeviceCapability {
10 Pose,
12 Camera,
14 Health,
16 Location,
18 Mic,
20 VendorReport,
22}
23
24impl DeviceCapability {
25 pub const ALL: [Self; 6] = [
27 Self::Pose,
28 Self::Camera,
29 Self::Health,
30 Self::Location,
31 Self::Mic,
32 Self::VendorReport,
33 ];
34
35 pub fn as_str(self) -> &'static str {
37 match self {
38 Self::Pose => "device/pose",
39 Self::Camera => "device/camera",
40 Self::Health => "device/health",
41 Self::Location => "device/location",
42 Self::Mic => "device/mic",
43 Self::VendorReport => "device/vendor-report",
44 }
45 }
46
47 pub fn capability_name(self) -> CapabilityName {
49 CapabilityName::new(self.as_str())
50 }
51
52 pub fn grant_symbol(self) -> Symbol {
54 Symbol::qualified("device", self.local_name())
55 }
56
57 pub fn from_name(name: &str) -> Option<Self> {
59 Self::ALL
60 .into_iter()
61 .find(|capability| capability.as_str() == name)
62 }
63
64 fn local_name(self) -> &'static str {
65 self.as_str()
66 .strip_prefix("device/")
67 .expect("device capability names keep the device/ prefix")
68 }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
73pub struct EdgeId(Symbol);
74
75impl EdgeId {
76 pub fn new(symbol: Symbol) -> Self {
78 Self(symbol)
79 }
80
81 pub fn named(name: impl Into<String>) -> Self {
83 Self(Symbol::qualified("device/session", name.into()))
84 }
85
86 pub fn as_symbol(&self) -> &Symbol {
88 &self.0
89 }
90
91 pub fn to_expr(&self) -> Expr {
93 Expr::Symbol(self.0.clone())
94 }
95
96 pub fn from_expr(expr: &Expr) -> Result<Self> {
98 match expr {
99 Expr::Symbol(symbol) => Ok(Self(symbol.clone())),
100 _ => Err(Error::TypeMismatch {
101 expected: "device edge id symbol",
102 found: "non-symbol",
103 }),
104 }
105 }
106}
107
108impl From<Symbol> for EdgeId {
109 fn from(symbol: Symbol) -> Self {
110 Self::new(symbol)
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct ConsentReceipt {
117 pub grants: Vec<Symbol>,
119 pub retain_ms: u64,
121 pub redact: Vec<Symbol>,
123 pub session: EdgeId,
125 pub seq: u64,
127}
128
129impl ConsentReceipt {
130 pub fn new(
132 grants: Vec<Symbol>,
133 retain_ms: u64,
134 redact: Vec<Symbol>,
135 session: EdgeId,
136 seq: u64,
137 ) -> Self {
138 Self {
139 grants: dedup_symbols(grants),
140 retain_ms,
141 redact: dedup_symbols(redact),
142 session,
143 seq,
144 }
145 }
146
147 pub fn to_expr(&self) -> Expr {
149 build::map(vec![
150 ("kind", build::qsym("device", "consent-receipt")),
151 ("grants", symbol_list(&self.grants)),
152 ("retain-ms", build::uint(self.retain_ms)),
153 ("redact", symbol_list(&self.redact)),
154 ("session", self.session.to_expr()),
155 ("seq", build::uint(self.seq)),
156 ])
157 }
158
159 pub fn from_expr(expr: &Expr) -> Result<Self> {
161 ensure_kind(expr)?;
162 let grants = symbol_vec(access::required(expr, "grants", "device consent receipt")?)?;
163 let retain_ms = uint_field(expr, "retain-ms")?;
164 let redact = symbol_vec(access::required(expr, "redact", "device consent receipt")?)?;
165 let session =
166 EdgeId::from_expr(access::required(expr, "session", "device consent receipt")?)?;
167 let seq = uint_field(expr, "seq")?;
168 Ok(Self::new(grants, retain_ms, redact, session, seq))
169 }
170
171 pub fn to_badge_scene(&self) -> Expr {
173 sim_lib_scene::badge("ok", &format!("consent {}", self.seq))
174 }
175
176 pub fn to_glance_scene(&self) -> Expr {
178 GlanceCard::new(
179 "Device consent",
180 Some(GlanceMetric::new(
181 "session",
182 self.session.as_symbol().as_qualified_str(),
183 )),
184 None,
185 "info",
186 1,
187 )
188 .to_scene()
189 }
190}
191
192pub fn record_consent_receipt(
195 ledger: &mut EventLedger,
196 run: Ref,
197 grants: Vec<Symbol>,
198 retain_ms: u64,
199 redact: Vec<Symbol>,
200 session: EdgeId,
201) -> Result<ConsentReceipt> {
202 let next_seq = ledger.len_for_run(&run) as u64;
203 let event = ledger.push(
204 run,
205 EventKind::Card {
206 subject: Ref::Symbol(session.as_symbol().clone()),
207 card: Ref::Symbol(Symbol::qualified(
208 "device/consent",
209 format!("receipt-{next_seq}"),
210 )),
211 },
212 )?;
213 Ok(ConsentReceipt::new(
214 grants, retain_ms, redact, session, event.seq,
215 ))
216}
217
218pub fn require_with_consent(
220 cx: &Cx,
221 name: &str,
222 receipt: &ConsentReceipt,
223 session: &EdgeId,
224) -> Result<()> {
225 cx.require(&CapabilityName::new(name.to_owned()))?;
226 if &receipt.session != session {
227 return Err(Error::HostError(format!(
228 "{name}: consent not for this session"
229 )));
230 }
231 if !receipt
232 .grants
233 .iter()
234 .any(|grant| grant_matches(grant, name))
235 {
236 return Err(Error::HostError(format!("{name} requires visible consent")));
237 }
238 Ok(())
239}
240
241fn ensure_kind(expr: &Expr) -> Result<()> {
242 match access::field_sym(expr, "kind") {
243 Some(kind)
244 if kind.namespace.as_deref() == Some("device")
245 && kind.name.as_ref() == "consent-receipt" =>
246 {
247 Ok(())
248 }
249 _ => Err(Error::HostError(
250 "expected device/consent-receipt".to_owned(),
251 )),
252 }
253}
254
255fn uint_field(expr: &Expr, name: &str) -> Result<u64> {
256 let value = access::required(expr, name, "device consent receipt")?;
257 match value {
258 Expr::Number(number) if number.domain.namespace.is_none() => number
259 .canonical
260 .parse()
261 .map_err(|_| Error::Eval(format!("device consent receipt field {name} is not u64"))),
262 _ => Err(Error::Eval(format!(
263 "device consent receipt field {name} is not u64"
264 ))),
265 }
266}
267
268fn symbol_vec(expr: &Expr) -> Result<Vec<Symbol>> {
269 match expr {
270 Expr::List(items) => items
271 .iter()
272 .map(|item| match item {
273 Expr::Symbol(symbol) => Ok(symbol.clone()),
274 _ => Err(Error::TypeMismatch {
275 expected: "symbol list",
276 found: "non-symbol",
277 }),
278 })
279 .collect(),
280 _ => Err(Error::TypeMismatch {
281 expected: "symbol list",
282 found: "non-list",
283 }),
284 }
285}
286
287fn symbol_list(symbols: &[Symbol]) -> Expr {
288 build::list(symbols.iter().cloned().map(Expr::Symbol).collect())
289}
290
291fn grant_matches(grant: &Symbol, name: &str) -> bool {
292 grant.as_qualified_str() == name
293}
294
295fn dedup_symbols(mut symbols: Vec<Symbol>) -> Vec<Symbol> {
296 symbols.sort();
297 symbols.dedup();
298 symbols
299}