1use std::{collections::BTreeSet, fmt, sync::Arc};
8
9use crate::{
10 error::{Error, Result},
11 id::Symbol,
12};
13
14#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
29pub struct CapabilityName(Arc<str>);
30
31impl CapabilityName {
32 pub fn new(name: impl Into<Arc<str>>) -> Self {
34 Self(name.into())
35 }
36
37 pub fn as_str(&self) -> &str {
39 &self.0
40 }
41
42 pub fn as_symbol(&self) -> Symbol {
44 Symbol::qualified("capability", self.as_str().to_owned())
45 }
46}
47
48impl fmt::Display for CapabilityName {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.write_str(self.as_str())
51 }
52}
53
54#[derive(Debug)]
69pub struct GrantSeat {
70 cx_id: u64,
75}
76
77impl GrantSeat {
78 pub(crate) fn for_cx(cx_id: u64) -> Self {
79 Self { cx_id }
80 }
81
82 #[cfg(feature = "test-support")]
86 pub fn for_test() -> Self {
87 Self { cx_id: 0 }
88 }
89
90 pub fn grant(&self, cx: &mut crate::Cx, capability: CapabilityName) {
100 assert!(
101 self.cx_id == 0 || self.cx_id == cx.grant_id(),
102 "a GrantSeat may only grant into the Cx it was minted with"
103 );
104 cx.grant_from_host(capability);
105 }
106
107 pub fn grant_named(&self, cx: &mut crate::Cx, capability: &'static str) {
109 self.grant(cx, CapabilityName::new(capability));
110 }
111}
112
113#[derive(Clone, Debug, Default, PartialEq, Eq)]
126pub struct CapabilitySet {
127 granted: BTreeSet<CapabilityName>,
128}
129
130impl CapabilitySet {
131 pub fn new() -> Self {
133 Self::default()
134 }
135
136 pub fn grant(mut self, capability: CapabilityName) -> Self {
138 self.granted.insert(capability);
139 self
140 }
141
142 pub fn insert(&mut self, capability: CapabilityName) {
144 self.granted.insert(capability);
145 }
146
147 pub fn contains(&self, capability: &CapabilityName) -> bool {
149 self.granted.contains(capability)
150 }
151
152 pub fn intersect(&self, other: &CapabilitySet) -> CapabilitySet {
157 self.iter()
158 .filter(|capability| other.contains(capability))
159 .cloned()
160 .fold(CapabilitySet::new(), CapabilitySet::grant)
161 }
162
163 pub fn iter(&self) -> impl Iterator<Item = &CapabilityName> {
165 self.granted.iter()
166 }
167}
168
169pub fn diminish(current: &CapabilitySet, allowed: &CapabilitySet) -> CapabilitySet {
175 current.intersect(allowed)
176}
177
178#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
183pub enum TrustLevel {
184 #[default]
186 Untrusted,
187 TrustedSource,
189 HostInternal,
191}
192
193#[derive(Clone, Debug, Default, PartialEq, Eq)]
198pub struct ReadPolicy {
199 pub trust: TrustLevel,
201 pub capabilities: CapabilitySet,
203}
204
205impl ReadPolicy {
206 pub fn require(&self, capability: &CapabilityName) -> Result<()> {
212 if !self.capabilities.contains(capability) {
213 return Err(Error::CapabilityDenied {
214 capability: capability.clone(),
215 });
216 }
217
218 if self.trust == TrustLevel::Untrusted && capability == &read_eval_capability() {
219 return Err(Error::TrustDenied {
220 capability: capability.clone(),
221 trust: self.trust,
222 });
223 }
224
225 Ok(())
226 }
227}
228
229pub fn read_construct_capability() -> CapabilityName {
231 CapabilityName::new("read-construct")
232}
233
234pub fn read_eval_capability() -> CapabilityName {
238 CapabilityName::new("read-eval")
239}
240
241pub fn native_dynamic_load_capability() -> CapabilityName {
243 CapabilityName::new("loader.native")
244}
245
246pub fn macro_expand_capability() -> CapabilityName {
248 CapabilityName::new("macro.expand")
249}
250
251pub fn macro_expand_compile_capability() -> CapabilityName {
253 CapabilityName::new("macro.expand.compile")
254}
255
256pub fn macro_expand_eval_capability() -> CapabilityName {
258 CapabilityName::new("macro.expand.eval")
259}
260
261pub fn macro_expand_read_capability() -> CapabilityName {
263 CapabilityName::new("macro.expand.read")
264}
265
266pub fn eval_fabric_capability() -> CapabilityName {
268 CapabilityName::new("eval.fabric")
269}
270
271pub fn eval_remote_capability() -> CapabilityName {
273 CapabilityName::new("eval.remote")
274}
275
276pub fn control_prompt_capability() -> CapabilityName {
278 CapabilityName::new("control.prompt")
279}
280
281pub fn control_capture_capability() -> CapabilityName {
283 CapabilityName::new("control.capture")
284}
285
286pub fn control_resume_capability() -> CapabilityName {
288 CapabilityName::new("control.resume")
289}
290
291pub fn control_multishot_capability() -> CapabilityName {
293 CapabilityName::new("control.multishot")
294}
295
296pub fn fact_private_capability() -> CapabilityName {
298 CapabilityName::new("kernel.fact.private")
299}
300
301pub fn browse_read_capability() -> CapabilityName {
303 CapabilityName::new("browse.read")
304}
305
306pub fn browse_run_tests_capability() -> CapabilityName {
308 CapabilityName::new("browse.run-tests")
309}
310
311pub fn browse_internal_capability() -> CapabilityName {
313 CapabilityName::new("browse.internal")
314}
315
316pub fn logic_db_write_capability() -> CapabilityName {
318 CapabilityName::new("logic.db.write")
319}
320
321pub fn logic_consult_file_capability() -> CapabilityName {
323 CapabilityName::new("logic.consult.file")
324}
325
326pub fn logic_tool_call_capability() -> CapabilityName {
328 CapabilityName::new("logic.tool-call")
329}
330
331pub fn config_list_impl_capability() -> CapabilityName {
333 CapabilityName::new("config.list.impl")
334}
335
336pub fn config_table_impl_capability() -> CapabilityName {
338 CapabilityName::new("config.table.impl")
339}
340
341pub fn list_force_unbounded_capability() -> CapabilityName {
343 CapabilityName::new("list.force.unbounded")
344}
345
346pub fn table_fs_capability() -> CapabilityName {
348 CapabilityName::new("table.fs")
349}
350
351pub fn table_fs_read_capability() -> CapabilityName {
353 CapabilityName::new("table.fs.read")
354}
355
356pub fn table_fs_write_capability() -> CapabilityName {
358 CapabilityName::new("table.fs.write")
359}
360
361pub fn table_fs_mkdir_capability() -> CapabilityName {
363 CapabilityName::new("table.fs.mkdir")
364}
365
366pub fn table_fs_rmdir_capability() -> CapabilityName {
368 CapabilityName::new("table.fs.rmdir")
369}
370
371pub fn table_db_capability() -> CapabilityName {
373 CapabilityName::new("table.db")
374}
375
376pub fn table_db_read_capability() -> CapabilityName {
378 CapabilityName::new("table.db.read")
379}
380
381pub fn table_db_write_capability() -> CapabilityName {
383 CapabilityName::new("table.db.write")
384}
385
386pub fn table_db_mkdir_capability() -> CapabilityName {
388 CapabilityName::new("table.db.mkdir")
389}
390
391pub fn table_db_rmdir_capability() -> CapabilityName {
393 CapabilityName::new("table.db.rmdir")
394}
395
396pub fn table_remote_capability() -> CapabilityName {
398 CapabilityName::new("table.remote")
399}
400
401pub fn registry_catalog_read_capability() -> CapabilityName {
403 CapabilityName::new("registry.catalog.read")
404}
405
406#[cfg(test)]
407mod tests {
408 use super::{
409 CapabilityName, CapabilitySet, ReadPolicy, TrustLevel, diminish, read_construct_capability,
410 read_eval_capability,
411 };
412 use crate::Error;
413
414 fn named_set(names: &[&str]) -> CapabilitySet {
415 names
416 .iter()
417 .copied()
418 .map(CapabilityName::new)
419 .fold(CapabilitySet::new(), CapabilitySet::grant)
420 }
421
422 fn policy(trust: TrustLevel, capabilities: &[crate::CapabilityName]) -> ReadPolicy {
423 ReadPolicy {
424 trust,
425 capabilities: capabilities
426 .iter()
427 .cloned()
428 .fold(CapabilitySet::new(), |set, capability| {
429 set.grant(capability)
430 }),
431 }
432 }
433
434 fn mask_set(universe: &[&str], mask: usize) -> CapabilitySet {
435 universe
436 .iter()
437 .enumerate()
438 .filter_map(|(index, name)| ((mask & (1 << index)) != 0).then_some(*name))
439 .map(CapabilityName::new)
440 .fold(CapabilitySet::new(), CapabilitySet::grant)
441 }
442
443 fn assert_subset(subset: &CapabilitySet, superset: &CapabilitySet) {
444 for capability in subset.iter() {
445 assert!(
446 superset.contains(capability),
447 "{capability} must be present in the superset"
448 );
449 }
450 }
451
452 #[test]
453 fn diminish_is_subset_of_current_and_allowed_for_small_sets() {
454 let universe = ["read-construct", "read-eval", "eval.fabric", "table.remote"];
455 for current_mask in 0..(1 << universe.len()) {
456 for allowed_mask in 0..(1 << universe.len()) {
457 let current = mask_set(&universe, current_mask);
458 let allowed = mask_set(&universe, allowed_mask);
459 let diminished = diminish(¤t, &allowed);
460 assert_subset(&diminished, ¤t);
461 assert_subset(&diminished, &allowed);
462
463 for name in universe {
464 let capability = CapabilityName::new(name);
465 assert_eq!(
466 diminished.contains(&capability),
467 current.contains(&capability) && allowed.contains(&capability),
468 "{name} membership should be exact set intersection"
469 );
470 }
471 }
472 }
473 }
474
475 #[test]
476 fn diminish_never_adds_a_capability_from_allowed_only() {
477 let current = named_set(&["read-construct"]);
478 let allowed = named_set(&["read-construct", "read-eval"]);
479 let diminished = diminish(¤t, &allowed);
480 assert!(diminished.contains(&read_construct_capability()));
481 assert!(!diminished.contains(&read_eval_capability()));
482 }
483
484 #[test]
485 fn untrusted_policy_denies_read_eval_even_when_capability_is_present() {
486 let err = policy(TrustLevel::Untrusted, &[read_eval_capability()])
487 .require(&read_eval_capability())
488 .unwrap_err();
489 assert!(matches!(
490 err,
491 Error::TrustDenied { capability, trust }
492 if capability == read_eval_capability() && trust == TrustLevel::Untrusted
493 ));
494 }
495
496 #[test]
497 fn trusted_policy_allows_read_eval_when_capability_is_present() {
498 policy(TrustLevel::TrustedSource, &[read_eval_capability()])
499 .require(&read_eval_capability())
500 .unwrap();
501 }
502
503 #[test]
504 fn non_eval_capabilities_remain_capability_gated() {
505 policy(TrustLevel::Untrusted, &[read_construct_capability()])
506 .require(&read_construct_capability())
507 .unwrap();
508 }
509
510 #[test]
511 fn grant_seat_grants_a_capability_into_a_cx() {
512 use std::sync::Arc;
513
514 use crate::{Cx, DefaultFactory, NoopEvalPolicy};
515
516 let cap = read_eval_capability();
517 let (mut cx, seat) = Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
518 assert!(cx.require(&cap).is_err());
520 seat.grant(&mut cx, cap.clone());
521 assert!(cx.require(&cap).is_ok());
522 }
523
524 #[test]
525 #[should_panic(expected = "minted with")]
526 fn a_seat_cannot_grant_into_a_foreign_cx() {
527 use std::sync::Arc;
528
529 use crate::{Cx, DefaultFactory, NoopEvalPolicy};
530
531 let (mut victim, _victim_seat) =
535 Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
536 let (_throwaway, forged) =
537 Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
538 forged.grant(&mut victim, read_eval_capability()); }
540
541 #[cfg(feature = "test-support")]
542 #[test]
543 fn grant_seat_for_test_is_available_under_the_feature() {
544 let _seat = super::GrantSeat::for_test();
546 }
547}