Skip to main content

frame_core/
capability.rs

1//! Native host ownership and enforcement of the capability contract.
2//!
3//! Contract values are re-exported from the beamr-free `frame-capability`
4//! crate. The mutable table is private to frame-core and is reached only
5//! through [`HostCapabilityFacade`]. A [`CapabilityChecker`] contains only a
6//! component's check path: its type has no grant, revoke, or inspection method.
7//! Native component entrypoints receive only values representable by
8//! [`crate::component::ChildArgument`] over BEAM, so neither this table nor the
9//! host facade can cross into component code. Future tool generation must hand
10//! out consuming operations/checkers, never the host facade.
11//!
12//! Runtime grants are intentionally not persisted. Composition should apply
13//! grants before starting components with declared needs. Starting first is
14//! safe and recoverable: checks deny until a later explicit grant.
15//! Filesystem and network kinds are contract-complete but have no I/O consumer
16//! in v1. Cross-component readers must check before every view resolution and
17//! use the view only for that resolve → read → drop act. Retaining a resolved
18//! view is outside this contract; bounded revocation drain is the named future
19//! upgrade if long-held views become necessary.
20
21use std::collections::HashMap;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::{Arc, Mutex};
24
25use arc_swap::ArcSwap;
26use thiserror::Error;
27
28use crate::event::EventHub;
29
30use frame_capability::ComponentId;
31pub use frame_capability::{
32    Capability, CapabilityDenied, CapabilityKind, CapabilityRequest, CapabilityScope, CheckVerdict,
33    Grant, GrantProvenance, NetworkHost, PathRoot, ScopeValidationError,
34};
35
36/// Host-owned grant state. Its mutation surface is deliberately crate-private.
37pub struct CapabilityTable {
38    components: ArcSwap<HashMap<ComponentId, Arc<ComponentGrants>>>,
39    mutations: Mutex<()>,
40}
41
42impl CapabilityTable {
43    pub(crate) fn new() -> Self {
44        Self {
45            components: ArcSwap::from_pointee(HashMap::new()),
46            mutations: Mutex::new(()),
47        }
48    }
49
50    pub(crate) fn register_component(
51        &self,
52        component_id: ComponentId,
53        needs: Vec<CapabilityRequest>,
54    ) -> Result<(), CapabilityMutationError> {
55        let _mutation = self
56            .mutations
57            .lock()
58            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
59        let mut components = (**self.components.load()).clone();
60        components.insert(
61            component_id,
62            Arc::new(ComponentGrants {
63                needs,
64                active: AtomicBool::new(true),
65                grants: ArcSwap::from_pointee(HashMap::new()),
66                mutations: Mutex::new(HashMap::new()),
67            }),
68        );
69        self.components.store(Arc::new(components));
70        Ok(())
71    }
72
73    pub(crate) fn remove_component(
74        &self,
75        component_id: ComponentId,
76    ) -> Result<(), CapabilityMutationError> {
77        let _table_mutation = self
78            .mutations
79            .lock()
80            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
81        let component = self.component(component_id)?;
82        let mut grants = component
83            .mutations
84            .lock()
85            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
86        component.active.store(false, Ordering::Release);
87        grants.clear();
88        component.grants.store(Arc::new(HashMap::new()));
89        drop(grants);
90        let mut components = (**self.components.load()).clone();
91        components.remove(&component_id);
92        self.components.store(Arc::new(components));
93        Ok(())
94    }
95
96    fn component(
97        &self,
98        component_id: ComponentId,
99    ) -> Result<Arc<ComponentGrants>, CapabilityMutationError> {
100        self.components
101            .load()
102            .get(&component_id)
103            .cloned()
104            .ok_or(CapabilityMutationError::NotRegistered { component_id })
105    }
106
107    fn grant(
108        &self,
109        component_id: ComponentId,
110        request: CapabilityRequest,
111        provenance: GrantProvenance,
112    ) -> Result<Grant, CapabilityMutationError> {
113        let component = self.component(component_id)?;
114        if !component.needs.contains(&request) {
115            return Err(CapabilityMutationError::UndeclaredNeed {
116                component_id,
117                request,
118            });
119        }
120        let mut grants = component
121            .mutations
122            .lock()
123            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
124        if !component.active.load(Ordering::Acquire) {
125            return Err(CapabilityMutationError::NotRegistered { component_id });
126        }
127        if let Some(current) = grants.get(&request) {
128            return Err(CapabilityMutationError::AlreadyGranted {
129                current: current.clone(),
130            });
131        }
132        let grant = Grant {
133            component_id,
134            request: request.clone(),
135            provenance,
136        };
137        grants.insert(request, grant.clone());
138        component.grants.store(Arc::new(grants.clone()));
139        Ok(grant)
140    }
141
142    fn revoke(
143        &self,
144        component_id: ComponentId,
145        request: &CapabilityRequest,
146    ) -> Result<RevokeOutcome, CapabilityMutationError> {
147        let component = self.component(component_id)?;
148        let mut grants = component
149            .mutations
150            .lock()
151            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
152        if !component.active.load(Ordering::Acquire) {
153            return Err(CapabilityMutationError::NotRegistered { component_id });
154        }
155        let removed = grants.remove(request);
156        if removed.is_some() {
157            component.grants.store(Arc::new(grants.clone()));
158        }
159        Ok(removed.map_or(RevokeOutcome::NotGranted, RevokeOutcome::Revoked))
160    }
161
162    fn grants_for(&self, component_id: ComponentId) -> Result<Vec<Grant>, CapabilityMutationError> {
163        let component = self.component(component_id)?;
164        if !component.active.load(Ordering::Acquire) {
165            return Err(CapabilityMutationError::NotRegistered { component_id });
166        }
167        let mut grants: Vec<_> = component.grants.load().values().cloned().collect();
168        sort_grants(&mut grants);
169        Ok(grants)
170    }
171
172    fn full_table(&self) -> Vec<Grant> {
173        let components = self.components.load();
174        let mut grants = Vec::new();
175        for component in components.values() {
176            if component.active.load(Ordering::Acquire) {
177                grants.extend(component.grants.load().values().cloned());
178            }
179        }
180        sort_grants(&mut grants);
181        grants
182    }
183}
184
185fn sort_grants(grants: &mut [Grant]) {
186    grants.sort_by(|left, right| {
187        left.component_id
188            .cmp(&right.component_id)
189            .then_with(|| left.request.cmp(&right.request))
190    });
191}
192
193struct ComponentGrants {
194    needs: Vec<CapabilityRequest>,
195    active: AtomicBool,
196    grants: ArcSwap<HashMap<CapabilityRequest, Grant>>,
197    mutations: Mutex<HashMap<CapabilityRequest, Grant>>,
198}
199
200/// Check-only retained path used by every host-mediated consuming act.
201///
202/// This handle deliberately carries no verdict. Every call reads current table
203/// state, so retaining the handle across revoke cannot retain authority.
204#[derive(Clone)]
205pub struct CapabilityChecker {
206    component_id: ComponentId,
207    component: Arc<ComponentGrants>,
208    events: Arc<EventHub>,
209}
210
211impl CapabilityChecker {
212    /// Returns the component identity whose current authority this checker evaluates.
213    #[must_use]
214    pub const fn component_id(&self) -> ComponentId {
215        self.component_id
216    }
217
218    /// Evaluates one consuming act against current grants and events one denial.
219    ///
220    /// # Errors
221    ///
222    /// Returns a synchronization error if table or event delivery state was
223    /// poisoned. Denial itself is a successful typed [`CheckVerdict::Denied`].
224    pub fn check(&self, capability: &Capability) -> Result<CheckVerdict, CapabilityCheckError> {
225        let valid = capability.validate().is_ok();
226        let declared = valid
227            && self
228                .component
229                .needs
230                .iter()
231                .any(|need| need.capability.covers(capability));
232        let allowed = self.component.active.load(Ordering::Acquire)
233            && valid
234            && self
235                .component
236                .grants
237                .load()
238                .values()
239                .any(|grant| grant.request.capability.covers(capability));
240        if allowed {
241            return Ok(CheckVerdict::Allowed);
242        }
243        let denial = CapabilityDenied {
244            component_id: self.component_id,
245            kind: capability.kind(),
246            scope: capability.scope(),
247            declared,
248        };
249        self.events
250            .publish_denial(denial.clone())
251            .map_err(|_| CapabilityCheckError::SynchronizationPoisoned)?;
252        Ok(CheckVerdict::Denied(denial))
253    }
254}
255
256/// The embedding host's only mutation and full-inspection surface.
257///
258/// This facade borrows the registry-owned table and cannot be serialized or
259/// represented as a BEAM term. Component-facing checkers expose no route back.
260pub struct HostCapabilityFacade<'a> {
261    table: &'a CapabilityTable,
262    events: Arc<EventHub>,
263}
264
265impl<'a> HostCapabilityFacade<'a> {
266    pub(crate) fn new(table: &'a CapabilityTable, events: Arc<EventHub>) -> Self {
267        Self { table, events }
268    }
269
270    /// Applies one explicit grant that exactly matches a manifest declaration.
271    ///
272    /// # Errors
273    ///
274    /// Refuses missing components, undeclared needs, duplicate active grants,
275    /// or poisoned synchronization with typed current-state information.
276    pub fn grant(
277        &self,
278        component_id: ComponentId,
279        request: CapabilityRequest,
280        provenance: GrantProvenance,
281    ) -> Result<Grant, CapabilityMutationError> {
282        self.table.grant(component_id, request, provenance)
283    }
284
285    /// Removes one exact grant, returning a typed idempotent no-op when absent.
286    ///
287    /// # Errors
288    ///
289    /// Refuses missing components or poisoned synchronization.
290    pub fn revoke(
291        &self,
292        component_id: ComponentId,
293        request: &CapabilityRequest,
294    ) -> Result<RevokeOutcome, CapabilityMutationError> {
295        self.table.revoke(component_id, request)
296    }
297
298    /// Returns active grants for one registered component, including provenance.
299    ///
300    /// # Errors
301    ///
302    /// Refuses missing components or poisoned synchronization.
303    pub fn grants_for(
304        &self,
305        component_id: ComponentId,
306    ) -> Result<Vec<Grant>, CapabilityMutationError> {
307        self.table.grants_for(component_id)
308    }
309
310    /// Returns a wait-free snapshot of every active grant row and provenance.
311    #[must_use]
312    pub fn full_table(&self) -> Vec<Grant> {
313        self.table.full_table()
314    }
315
316    /// Creates a retained check-only path for a registered component.
317    ///
318    /// # Errors
319    ///
320    /// Refuses missing components or poisoned synchronization.
321    pub fn checker(
322        &self,
323        component_id: ComponentId,
324    ) -> Result<CapabilityChecker, CapabilityMutationError> {
325        Ok(CapabilityChecker {
326            component_id,
327            component: self.table.component(component_id)?,
328            events: Arc::clone(&self.events),
329        })
330    }
331}
332
333/// Linearized result of idempotently revoking one exact grant.
334#[derive(Clone, Debug, Eq, PartialEq)]
335pub enum RevokeOutcome {
336    /// The active row was removed and returned for audit reconstruction.
337    Revoked(Grant),
338    /// No active row existed at the mutation point.
339    NotGranted,
340}
341
342/// A typed current-state refusal from the host grant table.
343#[derive(Clone, Debug, Error, Eq, PartialEq)]
344pub enum CapabilityMutationError {
345    /// The identity has no current registered incarnation.
346    #[error("component {component_id} is not registered in the capability table")]
347    NotRegistered {
348        /// Missing or removed identity.
349        component_id: ComponentId,
350    },
351    /// The requested row was absent from the component manifest.
352    #[error("component {component_id} did not declare capability need {request:?}")]
353    UndeclaredNeed {
354        /// Component that would receive authority.
355        component_id: ComponentId,
356        /// Exact row refused by the host.
357        request: CapabilityRequest,
358    },
359    /// The exact row already exists; provenance identifies its current state.
360    #[error("capability grant already exists: {current:?}")]
361    AlreadyGranted {
362        /// Active row observed at the mutation point.
363        current: Grant,
364    },
365    /// Grant table synchronization was poisoned by a panic.
366    #[error("capability table synchronization is poisoned")]
367    SynchronizationPoisoned,
368}
369
370/// Failure to produce and publish a fresh check verdict.
371#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
372pub enum CapabilityCheckError {
373    /// No current incarnation exists for the identity.
374    #[error("component {component_id} is not registered for capability checks")]
375    NotRegistered {
376        /// Missing identity.
377        component_id: ComponentId,
378    },
379    /// Grant table or lifecycle event synchronization was poisoned by a panic.
380    #[error("capability check synchronization is poisoned")]
381    SynchronizationPoisoned,
382}