1use std::{collections::BTreeSet, fmt, sync::Arc};
8
9use crate::{
10 error::{Error, Result},
11 eval::Phase,
12 id::Symbol,
13};
14
15#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
30pub struct CapabilityName(Arc<str>);
31
32impl CapabilityName {
33 pub fn new(name: impl Into<Arc<str>>) -> Self {
35 Self(name.into())
36 }
37
38 pub fn as_str(&self) -> &str {
40 &self.0
41 }
42
43 pub fn as_symbol(&self) -> Symbol {
45 Symbol::qualified("capability", self.as_str().to_owned())
46 }
47}
48
49impl fmt::Display for CapabilityName {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str(self.as_str())
52 }
53}
54
55#[derive(Debug)]
70pub struct GrantSeat {
71 cx_id: u64,
76}
77
78impl GrantSeat {
79 pub(crate) fn for_cx(cx_id: u64) -> Self {
80 Self { cx_id }
81 }
82
83 #[cfg(feature = "test-support")]
87 pub fn for_test() -> Self {
88 Self { cx_id: 0 }
89 }
90
91 pub fn grant(&self, cx: &mut crate::Cx, capability: CapabilityName) -> Result<()> {
101 if self.cx_id != 0 && self.cx_id != cx.grant_id() {
102 return Err(Error::ForeignGrantSeat);
103 }
104 cx.grant_from_host(capability);
105 Ok(())
106 }
107
108 pub fn grant_named(&self, cx: &mut crate::Cx, capability: &'static str) -> Result<()> {
110 self.grant(cx, CapabilityName::new(capability))
111 }
112}
113
114#[derive(Clone, Debug, Default, PartialEq, Eq)]
127pub struct CapabilitySet {
128 granted: BTreeSet<CapabilityName>,
129}
130
131impl CapabilitySet {
132 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn grant(mut self, capability: CapabilityName) -> Self {
139 self.granted.insert(capability);
140 self
141 }
142
143 pub fn insert(&mut self, capability: CapabilityName) {
145 self.granted.insert(capability);
146 }
147
148 pub fn contains(&self, capability: &CapabilityName) -> bool {
150 self.granted.contains(capability)
151 }
152
153 pub fn intersect(&self, other: &CapabilitySet) -> CapabilitySet {
158 self.iter()
159 .filter(|capability| other.contains(capability))
160 .cloned()
161 .fold(CapabilitySet::new(), CapabilitySet::grant)
162 }
163
164 pub fn iter(&self) -> impl Iterator<Item = &CapabilityName> {
166 self.granted.iter()
167 }
168}
169
170pub fn diminish(current: &CapabilitySet, allowed: &CapabilitySet) -> CapabilitySet {
176 current.intersect(allowed)
177}
178
179#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
184pub enum TrustLevel {
185 #[default]
187 Untrusted,
188 TrustedSource,
190 HostInternal,
192}
193
194#[derive(Clone, Debug, Default, PartialEq, Eq)]
199pub struct ReadPolicy {
200 pub trust: TrustLevel,
202 pub capabilities: CapabilitySet,
204}
205
206impl ReadPolicy {
207 pub fn require(&self, capability: &CapabilityName) -> Result<()> {
213 if !self.capabilities.contains(capability) {
214 return Err(Error::CapabilityDenied {
215 capability: capability.clone(),
216 });
217 }
218
219 if self.trust == TrustLevel::Untrusted && capability == &read_eval_capability() {
220 return Err(Error::TrustDenied {
221 capability: capability.clone(),
222 trust: self.trust,
223 });
224 }
225
226 Ok(())
227 }
228}
229
230pub fn read_construct_capability() -> CapabilityName {
232 CapabilityName::new("read-construct")
233}
234
235pub fn read_eval_capability() -> CapabilityName {
239 CapabilityName::new("read-eval")
240}
241
242pub fn native_dynamic_load_capability() -> CapabilityName {
244 CapabilityName::new("loader.native")
245}
246
247pub fn macro_expand_capability() -> CapabilityName {
249 CapabilityName::new("macro.expand")
250}
251
252pub fn macro_expansion_capability_for_phase(phase: Phase) -> CapabilityName {
254 match phase {
255 Phase::Read => macro_expand_read_capability(),
256 Phase::Expand => macro_expand_capability(),
257 Phase::Compile => macro_expand_compile_capability(),
258 Phase::Eval => macro_expand_eval_capability(),
259 }
260}
261
262pub fn macro_expand_compile_capability() -> CapabilityName {
264 CapabilityName::new("macro.expand.compile")
265}
266
267pub fn macro_expand_eval_capability() -> CapabilityName {
269 CapabilityName::new("macro.expand.eval")
270}
271
272pub fn macro_expand_read_capability() -> CapabilityName {
274 CapabilityName::new("macro.expand.read")
275}
276
277pub fn eval_fabric_capability() -> CapabilityName {
279 CapabilityName::new("eval.fabric")
280}
281
282pub fn eval_remote_capability() -> CapabilityName {
284 CapabilityName::new("eval.remote")
285}
286
287pub fn control_prompt_capability() -> CapabilityName {
289 CapabilityName::new("control.prompt")
290}
291
292pub fn control_capture_capability() -> CapabilityName {
294 CapabilityName::new("control.capture")
295}
296
297pub fn control_resume_capability() -> CapabilityName {
299 CapabilityName::new("control.resume")
300}
301
302pub fn control_multishot_capability() -> CapabilityName {
304 CapabilityName::new("control.multishot")
305}
306
307pub fn fact_private_capability() -> CapabilityName {
309 CapabilityName::new("kernel.fact.private")
310}
311
312pub fn list_force_unbounded_capability() -> CapabilityName {
314 CapabilityName::new("list.force.unbounded")
315}
316
317pub fn registry_catalog_read_capability() -> CapabilityName {
319 CapabilityName::new("registry.catalog.read")
320}
321
322#[cfg(test)]
323mod tests {
324 use super::{
325 CapabilityName, CapabilitySet, ReadPolicy, TrustLevel, diminish, read_construct_capability,
326 read_eval_capability,
327 };
328 use crate::Error;
329
330 fn named_set(names: &[&str]) -> CapabilitySet {
331 names
332 .iter()
333 .copied()
334 .map(CapabilityName::new)
335 .fold(CapabilitySet::new(), CapabilitySet::grant)
336 }
337
338 fn policy(trust: TrustLevel, capabilities: &[crate::CapabilityName]) -> ReadPolicy {
339 ReadPolicy {
340 trust,
341 capabilities: capabilities
342 .iter()
343 .cloned()
344 .fold(CapabilitySet::new(), |set, capability| {
345 set.grant(capability)
346 }),
347 }
348 }
349
350 fn mask_set(universe: &[&str], mask: usize) -> CapabilitySet {
351 universe
352 .iter()
353 .enumerate()
354 .filter_map(|(index, name)| ((mask & (1 << index)) != 0).then_some(*name))
355 .map(CapabilityName::new)
356 .fold(CapabilitySet::new(), CapabilitySet::grant)
357 }
358
359 fn assert_subset(subset: &CapabilitySet, superset: &CapabilitySet) {
360 for capability in subset.iter() {
361 assert!(
362 superset.contains(capability),
363 "{capability} must be present in the superset"
364 );
365 }
366 }
367
368 #[test]
369 fn diminish_is_subset_of_current_and_allowed_for_small_sets() {
370 let universe = ["read-construct", "read-eval", "eval.fabric", "eval.remote"];
371 for current_mask in 0..(1 << universe.len()) {
372 for allowed_mask in 0..(1 << universe.len()) {
373 let current = mask_set(&universe, current_mask);
374 let allowed = mask_set(&universe, allowed_mask);
375 let diminished = diminish(¤t, &allowed);
376 assert_subset(&diminished, ¤t);
377 assert_subset(&diminished, &allowed);
378
379 for name in universe {
380 let capability = CapabilityName::new(name);
381 assert_eq!(
382 diminished.contains(&capability),
383 current.contains(&capability) && allowed.contains(&capability),
384 "{name} membership should be exact set intersection"
385 );
386 }
387 }
388 }
389 }
390
391 #[test]
392 fn diminish_never_adds_a_capability_from_allowed_only() {
393 let current = named_set(&["read-construct"]);
394 let allowed = named_set(&["read-construct", "read-eval"]);
395 let diminished = diminish(¤t, &allowed);
396 assert!(diminished.contains(&read_construct_capability()));
397 assert!(!diminished.contains(&read_eval_capability()));
398 }
399
400 #[test]
401 fn untrusted_policy_denies_read_eval_even_when_capability_is_present() {
402 let err = policy(TrustLevel::Untrusted, &[read_eval_capability()])
403 .require(&read_eval_capability())
404 .unwrap_err();
405 assert!(matches!(
406 err,
407 Error::TrustDenied { capability, trust }
408 if capability == read_eval_capability() && trust == TrustLevel::Untrusted
409 ));
410 }
411
412 #[test]
413 fn trusted_policy_allows_read_eval_when_capability_is_present() {
414 policy(TrustLevel::TrustedSource, &[read_eval_capability()])
415 .require(&read_eval_capability())
416 .unwrap();
417 }
418
419 #[test]
420 fn non_eval_capabilities_remain_capability_gated() {
421 policy(TrustLevel::Untrusted, &[read_construct_capability()])
422 .require(&read_construct_capability())
423 .unwrap();
424 }
425
426 #[test]
427 fn grant_seat_grants_a_capability_into_a_cx() {
428 use std::sync::Arc;
429
430 use crate::{Cx, DefaultFactory, NoopEvalPolicy};
431
432 let cap = read_eval_capability();
433 let (mut cx, seat) = Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
434 assert!(cx.require(&cap).is_err());
436 seat.grant(&mut cx, cap.clone()).unwrap();
437 assert!(cx.require(&cap).is_ok());
438 }
439
440 #[test]
441 fn a_seat_cannot_grant_into_a_foreign_cx() {
442 use std::sync::Arc;
443
444 use crate::{Cx, DefaultFactory, NoopEvalPolicy};
445
446 let (mut victim, _victim_seat) =
450 Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
451 let (_throwaway, forged) =
452 Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
453 assert!(matches!(
454 forged.grant(&mut victim, read_eval_capability()),
455 Err(Error::ForeignGrantSeat)
456 ));
457 }
458
459 #[cfg(feature = "test-support")]
460 #[test]
461 fn grant_seat_for_test_is_available_under_the_feature() {
462 let _seat = super::GrantSeat::for_test();
464 }
465}