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    /// Iterates the granted capabilities in sorted order.
153    pub fn iter(&self) -> impl Iterator<Item = &CapabilityName> {
154        self.granted.iter()
155    }
156}
157
158/// The trust level of a source, gating capabilities beyond mere possession.
159///
160/// Even a granted capability may be denied when trust is insufficient; see
161/// [`ReadPolicy::require`].
162#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
163pub enum TrustLevel {
164    /// Untrusted input; eval-class capabilities are denied even if granted.
165    #[default]
166    Untrusted,
167    /// A source trusted to request evaluation.
168    TrustedSource,
169    /// The host itself, the most trusted level.
170    HostInternal,
171}
172
173/// The trust level and capability set governing a read.
174///
175/// Pairs a [`TrustLevel`] with a [`CapabilitySet`] so the kernel can both
176/// check possession and apply trust-sensitive policy.
177#[derive(Clone, Debug, Default, PartialEq, Eq)]
178pub struct ReadPolicy {
179    /// The trust level of the source being read.
180    pub trust: TrustLevel,
181    /// The capabilities granted to the read.
182    pub capabilities: CapabilitySet,
183}
184
185impl ReadPolicy {
186    /// Demands a capability, applying trust policy on top of possession.
187    ///
188    /// Returns [`Error::CapabilityDenied`] when the capability is not granted,
189    /// and [`Error::TrustDenied`] when an [`Untrusted`](TrustLevel::Untrusted)
190    /// source requests the read-eval capability.
191    pub fn require(&self, capability: &CapabilityName) -> Result<()> {
192        if !self.capabilities.contains(capability) {
193            return Err(Error::CapabilityDenied {
194                capability: capability.clone(),
195            });
196        }
197
198        if self.trust == TrustLevel::Untrusted && capability == &read_eval_capability() {
199            return Err(Error::TrustDenied {
200                capability: capability.clone(),
201                trust: self.trust,
202            });
203        }
204
205        Ok(())
206    }
207}
208
209/// The capability gating read-time construction (`read-construct`).
210pub fn read_construct_capability() -> CapabilityName {
211    CapabilityName::new("read-construct")
212}
213
214/// The capability gating read-time evaluation (`read-eval`).
215///
216/// Trust-sensitive: denied for [`Untrusted`](TrustLevel::Untrusted) sources.
217pub fn read_eval_capability() -> CapabilityName {
218    CapabilityName::new("read-eval")
219}
220
221/// The capability gating native dynamic library loading (`loader.native`).
222pub fn native_dynamic_load_capability() -> CapabilityName {
223    CapabilityName::new("loader.native")
224}
225
226/// The capability gating macro expansion (`macro.expand`).
227pub fn macro_expand_capability() -> CapabilityName {
228    CapabilityName::new("macro.expand")
229}
230
231/// The capability gating compile-phase macro expansion (`macro.expand.compile`).
232pub fn macro_expand_compile_capability() -> CapabilityName {
233    CapabilityName::new("macro.expand.compile")
234}
235
236/// The capability gating eval-phase macro expansion (`macro.expand.eval`).
237pub fn macro_expand_eval_capability() -> CapabilityName {
238    CapabilityName::new("macro.expand.eval")
239}
240
241/// The capability gating read-phase macro expansion (`macro.expand.read`).
242pub fn macro_expand_read_capability() -> CapabilityName {
243    CapabilityName::new("macro.expand.read")
244}
245
246/// The capability gating use of the eval fabric (`eval.fabric`).
247pub fn eval_fabric_capability() -> CapabilityName {
248    CapabilityName::new("eval.fabric")
249}
250
251/// The capability gating remote evaluation (`eval.remote`).
252pub fn eval_remote_capability() -> CapabilityName {
253    CapabilityName::new("eval.remote")
254}
255
256/// The capability gating control prompts (`control.prompt`).
257pub fn control_prompt_capability() -> CapabilityName {
258    CapabilityName::new("control.prompt")
259}
260
261/// The capability gating control-stack capture (`control.capture`).
262pub fn control_capture_capability() -> CapabilityName {
263    CapabilityName::new("control.capture")
264}
265
266/// The capability gating continuation resumption (`control.resume`).
267pub fn control_resume_capability() -> CapabilityName {
268    CapabilityName::new("control.resume")
269}
270
271/// The capability gating multi-shot continuations (`control.multishot`).
272pub fn control_multishot_capability() -> CapabilityName {
273    CapabilityName::new("control.multishot")
274}
275
276/// The capability gating access to private facts (`kernel.fact.private`).
277pub fn fact_private_capability() -> CapabilityName {
278    CapabilityName::new("kernel.fact.private")
279}
280
281/// The capability gating browse reads (`browse.read`).
282pub fn browse_read_capability() -> CapabilityName {
283    CapabilityName::new("browse.read")
284}
285
286/// The capability gating browse-driven test runs (`browse.run-tests`).
287pub fn browse_run_tests_capability() -> CapabilityName {
288    CapabilityName::new("browse.run-tests")
289}
290
291/// The capability gating internal browse surfaces (`browse.internal`).
292pub fn browse_internal_capability() -> CapabilityName {
293    CapabilityName::new("browse.internal")
294}
295
296/// The capability gating logic-database writes (`logic.db.write`).
297pub fn logic_db_write_capability() -> CapabilityName {
298    CapabilityName::new("logic.db.write")
299}
300
301/// The capability gating logic file consulting (`logic.consult.file`).
302pub fn logic_consult_file_capability() -> CapabilityName {
303    CapabilityName::new("logic.consult.file")
304}
305
306/// The capability gating logic tool calls (`logic.tool-call`).
307pub fn logic_tool_call_capability() -> CapabilityName {
308    CapabilityName::new("logic.tool-call")
309}
310
311/// The capability gating the configured list implementation (`config.list.impl`).
312pub fn config_list_impl_capability() -> CapabilityName {
313    CapabilityName::new("config.list.impl")
314}
315
316/// The capability gating the configured table implementation (`config.table.impl`).
317pub fn config_table_impl_capability() -> CapabilityName {
318    CapabilityName::new("config.table.impl")
319}
320
321/// The capability gating unbounded list forcing (`list.force.unbounded`).
322pub fn list_force_unbounded_capability() -> CapabilityName {
323    CapabilityName::new("list.force.unbounded")
324}
325
326/// The capability gating filesystem-backed tables (`table.fs`).
327pub fn table_fs_capability() -> CapabilityName {
328    CapabilityName::new("table.fs")
329}
330
331/// The capability gating filesystem table reads (`table.fs.read`).
332pub fn table_fs_read_capability() -> CapabilityName {
333    CapabilityName::new("table.fs.read")
334}
335
336/// The capability gating filesystem table writes (`table.fs.write`).
337pub fn table_fs_write_capability() -> CapabilityName {
338    CapabilityName::new("table.fs.write")
339}
340
341/// The capability gating filesystem table directory creation (`table.fs.mkdir`).
342pub fn table_fs_mkdir_capability() -> CapabilityName {
343    CapabilityName::new("table.fs.mkdir")
344}
345
346/// The capability gating filesystem table directory removal (`table.fs.rmdir`).
347pub fn table_fs_rmdir_capability() -> CapabilityName {
348    CapabilityName::new("table.fs.rmdir")
349}
350
351/// The capability gating database-backed tables (`table.db`).
352pub fn table_db_capability() -> CapabilityName {
353    CapabilityName::new("table.db")
354}
355
356/// The capability gating database table reads (`table.db.read`).
357pub fn table_db_read_capability() -> CapabilityName {
358    CapabilityName::new("table.db.read")
359}
360
361/// The capability gating database table writes (`table.db.write`).
362pub fn table_db_write_capability() -> CapabilityName {
363    CapabilityName::new("table.db.write")
364}
365
366/// The capability gating database table directory creation (`table.db.mkdir`).
367pub fn table_db_mkdir_capability() -> CapabilityName {
368    CapabilityName::new("table.db.mkdir")
369}
370
371/// The capability gating database table directory removal (`table.db.rmdir`).
372pub fn table_db_rmdir_capability() -> CapabilityName {
373    CapabilityName::new("table.db.rmdir")
374}
375
376/// The capability gating remote tables (`table.remote`).
377pub fn table_remote_capability() -> CapabilityName {
378    CapabilityName::new("table.remote")
379}
380
381/// The capability gating registry catalog reads (`registry.catalog.read`).
382pub fn registry_catalog_read_capability() -> CapabilityName {
383    CapabilityName::new("registry.catalog.read")
384}
385
386#[cfg(test)]
387mod tests {
388    use super::{
389        CapabilitySet, ReadPolicy, TrustLevel, read_construct_capability, read_eval_capability,
390    };
391    use crate::Error;
392
393    fn policy(trust: TrustLevel, capabilities: &[crate::CapabilityName]) -> ReadPolicy {
394        ReadPolicy {
395            trust,
396            capabilities: capabilities
397                .iter()
398                .cloned()
399                .fold(CapabilitySet::new(), |set, capability| {
400                    set.grant(capability)
401                }),
402        }
403    }
404
405    #[test]
406    fn untrusted_policy_denies_read_eval_even_when_capability_is_present() {
407        let err = policy(TrustLevel::Untrusted, &[read_eval_capability()])
408            .require(&read_eval_capability())
409            .unwrap_err();
410        assert!(matches!(
411            err,
412            Error::TrustDenied { capability, trust }
413                if capability == read_eval_capability() && trust == TrustLevel::Untrusted
414        ));
415    }
416
417    #[test]
418    fn trusted_policy_allows_read_eval_when_capability_is_present() {
419        policy(TrustLevel::TrustedSource, &[read_eval_capability()])
420            .require(&read_eval_capability())
421            .unwrap();
422    }
423
424    #[test]
425    fn non_eval_capabilities_remain_capability_gated() {
426        policy(TrustLevel::Untrusted, &[read_construct_capability()])
427            .require(&read_construct_capability())
428            .unwrap();
429    }
430
431    #[test]
432    fn grant_seat_grants_a_capability_into_a_cx() {
433        use std::sync::Arc;
434
435        use crate::{Cx, DefaultFactory, NoopEvalPolicy};
436
437        let cap = read_eval_capability();
438        let (mut cx, seat) = Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
439        // A freshly seated context grants nothing until the host uses the seat.
440        assert!(cx.require(&cap).is_err());
441        seat.grant(&mut cx, cap.clone());
442        assert!(cx.require(&cap).is_ok());
443    }
444
445    #[test]
446    #[should_panic(expected = "minted with")]
447    fn a_seat_cannot_grant_into_a_foreign_cx() {
448        use std::sync::Arc;
449
450        use crate::{Cx, DefaultFactory, NoopEvalPolicy};
451
452        // The escalation a loaded callable would attempt: it holds `victim`
453        // (its own &mut Cx) and mints a fresh throwaway pair, then tries to use
454        // the throwaway seat to grant into `victim`.
455        let (mut victim, _victim_seat) =
456            Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
457        let (_throwaway, forged) =
458            Cx::new_seated(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
459        forged.grant(&mut victim, read_eval_capability()); // panics: foreign Cx
460    }
461
462    #[cfg(feature = "test-support")]
463    #[test]
464    fn grant_seat_for_test_is_available_under_the_feature() {
465        // Compiles and runs only when `test-support` is enabled.
466        let _seat = super::GrantSeat::for_test();
467    }
468}