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, read policy, and
4//! cross-cutting kernel capability names. Libraries mint behavior-specific
5//! capability names with [`CapabilityName::new`] in their own crates.
6
7use std::{collections::BTreeSet, fmt, sync::Arc};
8
9use crate::{
10    error::{Error, Result},
11    eval::Phase,
12    id::Symbol,
13};
14
15/// The identity of a capability: the token an operation requires to run.
16///
17/// Capability names are the kernel's contract for gating privileged behavior;
18/// libraries decide what each name authorizes. See the README section
19/// "Capabilities and trust".
20///
21/// # Examples
22///
23/// ```
24/// # use sim_kernel::CapabilityName;
25/// let cap = CapabilityName::new("table.fs.read");
26/// assert_eq!(cap.as_str(), "table.fs.read");
27/// assert_eq!(cap.as_symbol().to_string(), "capability/table.fs.read");
28/// ```
29#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
30pub struct CapabilityName(Arc<str>);
31
32impl CapabilityName {
33    /// Interns a capability name from a string.
34    pub fn new(name: impl Into<Arc<str>>) -> Self {
35        Self(name.into())
36    }
37
38    /// Returns the capability name as a string slice.
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42
43    /// Returns the name as a `capability`-qualified [`Symbol`].
44    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/// A host-only capability-grant seat: the authority to grant a capability into a
56/// [`Cx`].
57///
58/// A `GrantSeat` is minted ONLY when a context is constructed
59/// ([`Cx::new_seated`]) or, under the `test-support` feature, by
60/// `GrantSeat::for_test`. It has no public constructor otherwise, so it cannot
61/// be forged. Crucially, it is never passed into [`Callable::call`] -- a loaded
62/// callable receives a `&mut Cx` but no seat, so it cannot grant itself a
63/// capability. Host code (a bootloader, a server session, a loader) holds the
64/// seat and grants through it.
65///
66/// [`Cx`]: crate::Cx
67/// [`Cx::new_seated`]: crate::Cx::new_seated
68/// [`Callable::call`]: crate::Callable::call
69#[derive(Debug)]
70pub struct GrantSeat {
71    /// The [`Cx`] identity this seat may grant into. `0` is a test-only wildcard
72    /// that matches any context (see [`GrantSeat::for_test`]).
73    ///
74    /// [`Cx`]: crate::Cx
75    cx_id: u64,
76}
77
78impl GrantSeat {
79    pub(crate) fn for_cx(cx_id: u64) -> Self {
80        Self { cx_id }
81    }
82
83    /// Mints a wildcard seat for tests and fixtures (grants into any context).
84    /// Only available under the `test-support` feature; never compiled into a
85    /// production build.
86    #[cfg(feature = "test-support")]
87    pub fn for_test() -> Self {
88        Self { cx_id: 0 }
89    }
90
91    /// Grants a capability into `cx` under this host authority.
92    ///
93    /// A seat may only grant into the context it was minted with
94    /// ([`Cx::new_seated`]). Minting a fresh `(Cx, GrantSeat)` pair yields a seat
95    /// bound to the fresh context, so it cannot be used to escalate a different
96    /// (e.g. the caller's own) context -- this is what keeps a loaded callable
97    /// from granting itself a capability.
98    ///
99    /// [`Cx::new_seated`]: crate::Cx::new_seated
100    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    /// Grants a capability named by a static string into `cx`.
109    pub fn grant_named(&self, cx: &mut crate::Cx, capability: &'static str) -> Result<()> {
110        self.grant(cx, CapabilityName::new(capability))
111    }
112}
113
114/// A set of granted capabilities, used as the capability state of a [`Cx`].
115///
116/// [`Cx`]: crate::Cx
117///
118/// # Examples
119///
120/// ```
121/// # use sim_kernel::{CapabilityName, capability::CapabilitySet};
122/// let cap = CapabilityName::new("read-construct");
123/// let granted = CapabilitySet::new().grant(cap.clone());
124/// assert!(granted.contains(&cap));
125/// ```
126#[derive(Clone, Debug, Default, PartialEq, Eq)]
127pub struct CapabilitySet {
128    granted: BTreeSet<CapabilityName>,
129}
130
131impl CapabilitySet {
132    /// Builds an empty capability set.
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    /// Returns the set with one more capability granted (builder style).
138    pub fn grant(mut self, capability: CapabilityName) -> Self {
139        self.granted.insert(capability);
140        self
141    }
142
143    /// Grants a capability in place.
144    pub fn insert(&mut self, capability: CapabilityName) {
145        self.granted.insert(capability);
146    }
147
148    /// Reports whether the capability is granted.
149    pub fn contains(&self, capability: &CapabilityName) -> bool {
150        self.granted.contains(capability)
151    }
152
153    /// Returns the capabilities present in both sets.
154    ///
155    /// Monotone: the result is a subset of each input, so using it as an active
156    /// capability set can only narrow authority, never widen it.
157    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    /// Iterates the granted capabilities in sorted order.
165    pub fn iter(&self) -> impl Iterator<Item = &CapabilityName> {
166        self.granted.iter()
167    }
168}
169
170/// Computes the diminished capability set for an explicitly narrowed run.
171///
172/// The result contains exactly the capabilities the caller already holds and
173/// the request explicitly allows. It is therefore a subset of both inputs and
174/// never grants a capability absent from `current`.
175pub fn diminish(current: &CapabilitySet, allowed: &CapabilitySet) -> CapabilitySet {
176    current.intersect(allowed)
177}
178
179/// The trust level of a source, gating capabilities beyond mere possession.
180///
181/// Even a granted capability may be denied when trust is insufficient; see
182/// [`ReadPolicy::require`].
183#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
184pub enum TrustLevel {
185    /// Untrusted input; eval-class capabilities are denied even if granted.
186    #[default]
187    Untrusted,
188    /// A source trusted to request evaluation.
189    TrustedSource,
190    /// The host itself, the most trusted level.
191    HostInternal,
192}
193
194/// The trust level and capability set governing a read.
195///
196/// Pairs a [`TrustLevel`] with a [`CapabilitySet`] so the kernel can both
197/// check possession and apply trust-sensitive policy.
198#[derive(Clone, Debug, Default, PartialEq, Eq)]
199pub struct ReadPolicy {
200    /// The trust level of the source being read.
201    pub trust: TrustLevel,
202    /// The capabilities granted to the read.
203    pub capabilities: CapabilitySet,
204}
205
206impl ReadPolicy {
207    /// Demands a capability, applying trust policy on top of possession.
208    ///
209    /// Returns [`Error::CapabilityDenied`] when the capability is not granted,
210    /// and [`Error::TrustDenied`] when an [`Untrusted`](TrustLevel::Untrusted)
211    /// source requests the read-eval capability.
212    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
230/// The capability gating read-time construction (`read-construct`).
231pub fn read_construct_capability() -> CapabilityName {
232    CapabilityName::new("read-construct")
233}
234
235/// The capability gating read-time evaluation (`read-eval`).
236///
237/// Trust-sensitive: denied for [`Untrusted`](TrustLevel::Untrusted) sources.
238pub fn read_eval_capability() -> CapabilityName {
239    CapabilityName::new("read-eval")
240}
241
242/// The capability gating native dynamic library loading (`loader.native`).
243pub fn native_dynamic_load_capability() -> CapabilityName {
244    CapabilityName::new("loader.native")
245}
246
247/// The capability gating macro expansion (`macro.expand`).
248pub fn macro_expand_capability() -> CapabilityName {
249    CapabilityName::new("macro.expand")
250}
251
252/// The phase-specific capability required to run macro expansion.
253pub 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
262/// The capability gating compile-phase macro expansion (`macro.expand.compile`).
263pub fn macro_expand_compile_capability() -> CapabilityName {
264    CapabilityName::new("macro.expand.compile")
265}
266
267/// The capability gating eval-phase macro expansion (`macro.expand.eval`).
268pub fn macro_expand_eval_capability() -> CapabilityName {
269    CapabilityName::new("macro.expand.eval")
270}
271
272/// The capability gating read-phase macro expansion (`macro.expand.read`).
273pub fn macro_expand_read_capability() -> CapabilityName {
274    CapabilityName::new("macro.expand.read")
275}
276
277/// The capability gating use of the eval fabric (`eval.fabric`).
278pub fn eval_fabric_capability() -> CapabilityName {
279    CapabilityName::new("eval.fabric")
280}
281
282/// The capability gating remote evaluation (`eval.remote`).
283pub fn eval_remote_capability() -> CapabilityName {
284    CapabilityName::new("eval.remote")
285}
286
287/// The capability gating control prompts (`control.prompt`).
288pub fn control_prompt_capability() -> CapabilityName {
289    CapabilityName::new("control.prompt")
290}
291
292/// The capability gating control-stack capture (`control.capture`).
293pub fn control_capture_capability() -> CapabilityName {
294    CapabilityName::new("control.capture")
295}
296
297/// The capability gating continuation resumption (`control.resume`).
298pub fn control_resume_capability() -> CapabilityName {
299    CapabilityName::new("control.resume")
300}
301
302/// The capability gating multi-shot continuations (`control.multishot`).
303pub fn control_multishot_capability() -> CapabilityName {
304    CapabilityName::new("control.multishot")
305}
306
307/// The capability gating access to private facts (`kernel.fact.private`).
308pub fn fact_private_capability() -> CapabilityName {
309    CapabilityName::new("kernel.fact.private")
310}
311
312/// The capability gating unbounded list forcing (`list.force.unbounded`).
313pub fn list_force_unbounded_capability() -> CapabilityName {
314    CapabilityName::new("list.force.unbounded")
315}
316
317/// The capability gating registry catalog reads (`registry.catalog.read`).
318pub 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(&current, &allowed);
376                assert_subset(&diminished, &current);
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(&current, &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        // A freshly seated context grants nothing until the host uses the seat.
435        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        // The escalation a loaded callable would attempt: it holds `victim`
447        // (its own &mut Cx) and mints a fresh throwaway pair, then tries to use
448        // the throwaway seat to grant into `victim`.
449        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        // Compiles and runs only when `test-support` is enabled.
463        let _seat = super::GrantSeat::for_test();
464    }
465}