Skip to main content

sim_kernel/
capability.rs

1//! Capability names and sets: the contract for gating privileged operations.
2//!
3//! The kernel defines capability identity, trust levels, and read policy plus
4//! the well-known core capability names; libraries decide what each capability
5//! authorizes.
6
7use std::{collections::BTreeSet, fmt, sync::Arc};
8
9use crate::{
10    error::{Error, Result},
11    id::Symbol,
12};
13
14/// The identity of a capability: the token an operation requires to run.
15///
16/// Capability names are the kernel's contract for gating privileged behavior;
17/// libraries decide what each name authorizes. See the README section
18/// "Capabilities and trust".
19///
20/// # Examples
21///
22/// ```
23/// # use sim_kernel::CapabilityName;
24/// let cap = CapabilityName::new("table.fs.read");
25/// assert_eq!(cap.as_str(), "table.fs.read");
26/// assert_eq!(cap.as_symbol().to_string(), "capability/table.fs.read");
27/// ```
28#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
29pub struct CapabilityName(Arc<str>);
30
31impl CapabilityName {
32    /// Interns a capability name from a string.
33    pub fn new(name: impl Into<Arc<str>>) -> Self {
34        Self(name.into())
35    }
36
37    /// Returns the capability name as a string slice.
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41
42    /// Returns the name as a `capability`-qualified [`Symbol`].
43    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/// A host-only capability-grant seat: the authority to grant a capability into a
55/// [`Cx`].
56///
57/// A `GrantSeat` is minted ONLY when a context is constructed
58/// ([`Cx::new_seated`]) or, under the `test-support` feature, by
59/// `GrantSeat::for_test`. It has no public constructor otherwise, so it cannot
60/// be forged. Crucially, it is never passed into [`Callable::call`] -- a loaded
61/// callable receives a `&mut Cx` but no seat, so it cannot grant itself a
62/// capability. Host code (a bootloader, a server session, a loader) holds the
63/// seat and grants through it.
64///
65/// [`Cx`]: crate::Cx
66/// [`Cx::new_seated`]: crate::Cx::new_seated
67/// [`Callable::call`]: crate::Callable::call
68#[derive(Debug)]
69pub struct GrantSeat {
70    /// The [`Cx`] identity this seat may grant into. `0` is a test-only wildcard
71    /// that matches any context (see [`GrantSeat::for_test`]).
72    ///
73    /// [`Cx`]: crate::Cx
74    cx_id: u64,
75}
76
77impl GrantSeat {
78    pub(crate) fn for_cx(cx_id: u64) -> Self {
79        Self { cx_id }
80    }
81
82    /// Mints a wildcard seat for tests and fixtures (grants into any context).
83    /// Only available under the `test-support` feature; never compiled into a
84    /// production build.
85    #[cfg(feature = "test-support")]
86    pub fn for_test() -> Self {
87        Self { cx_id: 0 }
88    }
89
90    /// Grants a capability into `cx` under this host authority.
91    ///
92    /// A seat may only grant into the context it was minted with
93    /// ([`Cx::new_seated`]). Minting a fresh `(Cx, GrantSeat)` pair yields a seat
94    /// bound to the fresh context, so it cannot be used to escalate a different
95    /// (e.g. the caller's own) context -- this is what keeps a loaded callable
96    /// from granting itself a capability.
97    ///
98    /// [`Cx::new_seated`]: crate::Cx::new_seated
99    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    /// Grants a capability named by a static string into `cx`.
108    pub fn grant_named(&self, cx: &mut crate::Cx, capability: &'static str) {
109        self.grant(cx, CapabilityName::new(capability));
110    }
111}
112
113/// A set of granted capabilities, used as the capability state of a [`Cx`].
114///
115/// [`Cx`]: crate::Cx
116///
117/// # Examples
118///
119/// ```
120/// # use sim_kernel::{CapabilityName, capability::CapabilitySet};
121/// let cap = CapabilityName::new("read-construct");
122/// let granted = CapabilitySet::new().grant(cap.clone());
123/// assert!(granted.contains(&cap));
124/// ```
125#[derive(Clone, Debug, Default, PartialEq, Eq)]
126pub struct CapabilitySet {
127    granted: BTreeSet<CapabilityName>,
128}
129
130impl CapabilitySet {
131    /// Builds an empty capability set.
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    /// Returns the set with one more capability granted (builder style).
137    pub fn grant(mut self, capability: CapabilityName) -> Self {
138        self.granted.insert(capability);
139        self
140    }
141
142    /// Grants a capability in place.
143    pub fn insert(&mut self, capability: CapabilityName) {
144        self.granted.insert(capability);
145    }
146
147    /// Reports whether the capability is granted.
148    pub fn contains(&self, capability: &CapabilityName) -> bool {
149        self.granted.contains(capability)
150    }
151
152    /// Returns the capabilities present in both sets.
153    ///
154    /// Monotone: the result is a subset of each input, so using it as an active
155    /// capability set can only narrow authority, never widen it.
156    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    /// Iterates the granted capabilities in sorted order.
164    pub fn iter(&self) -> impl Iterator<Item = &CapabilityName> {
165        self.granted.iter()
166    }
167}
168
169/// Computes the diminished capability set for an explicitly narrowed run.
170///
171/// The result contains exactly the capabilities the caller already holds and
172/// the request explicitly allows. It is therefore a subset of both inputs and
173/// never grants a capability absent from `current`.
174pub fn diminish(current: &CapabilitySet, allowed: &CapabilitySet) -> CapabilitySet {
175    current.intersect(allowed)
176}
177
178/// The trust level of a source, gating capabilities beyond mere possession.
179///
180/// Even a granted capability may be denied when trust is insufficient; see
181/// [`ReadPolicy::require`].
182#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
183pub enum TrustLevel {
184    /// Untrusted input; eval-class capabilities are denied even if granted.
185    #[default]
186    Untrusted,
187    /// A source trusted to request evaluation.
188    TrustedSource,
189    /// The host itself, the most trusted level.
190    HostInternal,
191}
192
193/// The trust level and capability set governing a read.
194///
195/// Pairs a [`TrustLevel`] with a [`CapabilitySet`] so the kernel can both
196/// check possession and apply trust-sensitive policy.
197#[derive(Clone, Debug, Default, PartialEq, Eq)]
198pub struct ReadPolicy {
199    /// The trust level of the source being read.
200    pub trust: TrustLevel,
201    /// The capabilities granted to the read.
202    pub capabilities: CapabilitySet,
203}
204
205impl ReadPolicy {
206    /// Demands a capability, applying trust policy on top of possession.
207    ///
208    /// Returns [`Error::CapabilityDenied`] when the capability is not granted,
209    /// and [`Error::TrustDenied`] when an [`Untrusted`](TrustLevel::Untrusted)
210    /// source requests the read-eval capability.
211    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
229/// The capability gating read-time construction (`read-construct`).
230pub fn read_construct_capability() -> CapabilityName {
231    CapabilityName::new("read-construct")
232}
233
234/// The capability gating read-time evaluation (`read-eval`).
235///
236/// Trust-sensitive: denied for [`Untrusted`](TrustLevel::Untrusted) sources.
237pub fn read_eval_capability() -> CapabilityName {
238    CapabilityName::new("read-eval")
239}
240
241/// The capability gating native dynamic library loading (`loader.native`).
242pub fn native_dynamic_load_capability() -> CapabilityName {
243    CapabilityName::new("loader.native")
244}
245
246/// The capability gating macro expansion (`macro.expand`).
247pub fn macro_expand_capability() -> CapabilityName {
248    CapabilityName::new("macro.expand")
249}
250
251/// The capability gating compile-phase macro expansion (`macro.expand.compile`).
252pub fn macro_expand_compile_capability() -> CapabilityName {
253    CapabilityName::new("macro.expand.compile")
254}
255
256/// The capability gating eval-phase macro expansion (`macro.expand.eval`).
257pub fn macro_expand_eval_capability() -> CapabilityName {
258    CapabilityName::new("macro.expand.eval")
259}
260
261/// The capability gating read-phase macro expansion (`macro.expand.read`).
262pub fn macro_expand_read_capability() -> CapabilityName {
263    CapabilityName::new("macro.expand.read")
264}
265
266/// The capability gating use of the eval fabric (`eval.fabric`).
267pub fn eval_fabric_capability() -> CapabilityName {
268    CapabilityName::new("eval.fabric")
269}
270
271/// The capability gating remote evaluation (`eval.remote`).
272pub fn eval_remote_capability() -> CapabilityName {
273    CapabilityName::new("eval.remote")
274}
275
276/// The capability gating control prompts (`control.prompt`).
277pub fn control_prompt_capability() -> CapabilityName {
278    CapabilityName::new("control.prompt")
279}
280
281/// The capability gating control-stack capture (`control.capture`).
282pub fn control_capture_capability() -> CapabilityName {
283    CapabilityName::new("control.capture")
284}
285
286/// The capability gating continuation resumption (`control.resume`).
287pub fn control_resume_capability() -> CapabilityName {
288    CapabilityName::new("control.resume")
289}
290
291/// The capability gating multi-shot continuations (`control.multishot`).
292pub fn control_multishot_capability() -> CapabilityName {
293    CapabilityName::new("control.multishot")
294}
295
296/// The capability gating access to private facts (`kernel.fact.private`).
297pub fn fact_private_capability() -> CapabilityName {
298    CapabilityName::new("kernel.fact.private")
299}
300
301/// The capability gating browse reads (`browse.read`).
302pub fn browse_read_capability() -> CapabilityName {
303    CapabilityName::new("browse.read")
304}
305
306/// The capability gating browse-driven test runs (`browse.run-tests`).
307pub fn browse_run_tests_capability() -> CapabilityName {
308    CapabilityName::new("browse.run-tests")
309}
310
311/// The capability gating internal browse surfaces (`browse.internal`).
312pub fn browse_internal_capability() -> CapabilityName {
313    CapabilityName::new("browse.internal")
314}
315
316/// The capability gating logic-database writes (`logic.db.write`).
317pub fn logic_db_write_capability() -> CapabilityName {
318    CapabilityName::new("logic.db.write")
319}
320
321/// The capability gating logic file consulting (`logic.consult.file`).
322pub fn logic_consult_file_capability() -> CapabilityName {
323    CapabilityName::new("logic.consult.file")
324}
325
326/// The capability gating logic tool calls (`logic.tool-call`).
327pub fn logic_tool_call_capability() -> CapabilityName {
328    CapabilityName::new("logic.tool-call")
329}
330
331/// The capability gating the configured list implementation (`config.list.impl`).
332pub fn config_list_impl_capability() -> CapabilityName {
333    CapabilityName::new("config.list.impl")
334}
335
336/// The capability gating the configured table implementation (`config.table.impl`).
337pub fn config_table_impl_capability() -> CapabilityName {
338    CapabilityName::new("config.table.impl")
339}
340
341/// The capability gating unbounded list forcing (`list.force.unbounded`).
342pub fn list_force_unbounded_capability() -> CapabilityName {
343    CapabilityName::new("list.force.unbounded")
344}
345
346/// The capability gating filesystem-backed tables (`table.fs`).
347pub fn table_fs_capability() -> CapabilityName {
348    CapabilityName::new("table.fs")
349}
350
351/// The capability gating filesystem table reads (`table.fs.read`).
352pub fn table_fs_read_capability() -> CapabilityName {
353    CapabilityName::new("table.fs.read")
354}
355
356/// The capability gating filesystem table writes (`table.fs.write`).
357pub fn table_fs_write_capability() -> CapabilityName {
358    CapabilityName::new("table.fs.write")
359}
360
361/// The capability gating filesystem table directory creation (`table.fs.mkdir`).
362pub fn table_fs_mkdir_capability() -> CapabilityName {
363    CapabilityName::new("table.fs.mkdir")
364}
365
366/// The capability gating filesystem table directory removal (`table.fs.rmdir`).
367pub fn table_fs_rmdir_capability() -> CapabilityName {
368    CapabilityName::new("table.fs.rmdir")
369}
370
371/// The capability gating database-backed tables (`table.db`).
372pub fn table_db_capability() -> CapabilityName {
373    CapabilityName::new("table.db")
374}
375
376/// The capability gating database table reads (`table.db.read`).
377pub fn table_db_read_capability() -> CapabilityName {
378    CapabilityName::new("table.db.read")
379}
380
381/// The capability gating database table writes (`table.db.write`).
382pub fn table_db_write_capability() -> CapabilityName {
383    CapabilityName::new("table.db.write")
384}
385
386/// The capability gating database table directory creation (`table.db.mkdir`).
387pub fn table_db_mkdir_capability() -> CapabilityName {
388    CapabilityName::new("table.db.mkdir")
389}
390
391/// The capability gating database table directory removal (`table.db.rmdir`).
392pub fn table_db_rmdir_capability() -> CapabilityName {
393    CapabilityName::new("table.db.rmdir")
394}
395
396/// The capability gating remote tables (`table.remote`).
397pub fn table_remote_capability() -> CapabilityName {
398    CapabilityName::new("table.remote")
399}
400
401/// The capability gating registry catalog reads (`registry.catalog.read`).
402pub 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(&current, &allowed);
460                assert_subset(&diminished, &current);
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(&current, &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        // A freshly seated context grants nothing until the host uses the seat.
519        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        // The escalation a loaded callable would attempt: it holds `victim`
532        // (its own &mut Cx) and mints a fresh throwaway pair, then tries to use
533        // the throwaway seat to grant into `victim`.
534        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()); // panics: foreign Cx
539    }
540
541    #[cfg(feature = "test-support")]
542    #[test]
543    fn grant_seat_for_test_is_available_under_the_feature() {
544        // Compiles and runs only when `test-support` is enabled.
545        let _seat = super::GrantSeat::for_test();
546    }
547}