frame-core 0.2.0

Component model, lifecycle, process isolation, and WASM module host
Documentation
//! Native host ownership and enforcement of the capability contract.
//!
//! Contract values are re-exported from the beamr-free `frame-capability`
//! crate. The mutable table is private to frame-core and is reached only
//! through [`HostCapabilityFacade`]. A [`CapabilityChecker`] contains only a
//! component's check path: its type has no grant, revoke, or inspection method.
//! Native component entrypoints receive only values representable by
//! [`crate::component::ChildArgument`] over BEAM, so neither this table nor the
//! host facade can cross into component code. Future tool generation must hand
//! out consuming operations/checkers, never the host facade.
//!
//! Runtime grants are intentionally not persisted. Composition should apply
//! grants before starting components with declared needs. Starting first is
//! safe and recoverable: checks deny until a later explicit grant.
//! Filesystem and network kinds are contract-complete but have no I/O consumer
//! in v1. Cross-component readers must check before every view resolution and
//! use the view only for that resolve → read → drop act. Retaining a resolved
//! view is outside this contract; bounded revocation drain is the named future
//! upgrade if long-held views become necessary.

use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};

use arc_swap::ArcSwap;
use thiserror::Error;

use crate::event::EventHub;

use frame_capability::ComponentId;
pub use frame_capability::{
    Capability, CapabilityDenied, CapabilityKind, CapabilityRequest, CapabilityScope, CheckVerdict,
    Grant, GrantProvenance, NetworkHost, PathRoot, ScopeValidationError,
};

/// Host-owned grant state. Its mutation surface is deliberately crate-private.
pub struct CapabilityTable {
    components: ArcSwap<HashMap<ComponentId, Arc<ComponentGrants>>>,
    mutations: Mutex<()>,
}

impl CapabilityTable {
    pub(crate) fn new() -> Self {
        Self {
            components: ArcSwap::from_pointee(HashMap::new()),
            mutations: Mutex::new(()),
        }
    }

    pub(crate) fn register_component(
        &self,
        component_id: ComponentId,
        needs: Vec<CapabilityRequest>,
    ) -> Result<(), CapabilityMutationError> {
        let _mutation = self
            .mutations
            .lock()
            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
        let mut components = (**self.components.load()).clone();
        components.insert(
            component_id,
            Arc::new(ComponentGrants {
                needs,
                active: AtomicBool::new(true),
                grants: ArcSwap::from_pointee(HashMap::new()),
                mutations: Mutex::new(HashMap::new()),
            }),
        );
        self.components.store(Arc::new(components));
        Ok(())
    }

    pub(crate) fn remove_component(
        &self,
        component_id: ComponentId,
    ) -> Result<(), CapabilityMutationError> {
        let _table_mutation = self
            .mutations
            .lock()
            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
        let component = self.component(component_id)?;
        let mut grants = component
            .mutations
            .lock()
            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
        component.active.store(false, Ordering::Release);
        grants.clear();
        component.grants.store(Arc::new(HashMap::new()));
        drop(grants);
        let mut components = (**self.components.load()).clone();
        components.remove(&component_id);
        self.components.store(Arc::new(components));
        Ok(())
    }

    fn component(
        &self,
        component_id: ComponentId,
    ) -> Result<Arc<ComponentGrants>, CapabilityMutationError> {
        self.components
            .load()
            .get(&component_id)
            .cloned()
            .ok_or(CapabilityMutationError::NotRegistered { component_id })
    }

    fn grant(
        &self,
        component_id: ComponentId,
        request: CapabilityRequest,
        provenance: GrantProvenance,
    ) -> Result<Grant, CapabilityMutationError> {
        let component = self.component(component_id)?;
        if !component.needs.contains(&request) {
            return Err(CapabilityMutationError::UndeclaredNeed {
                component_id,
                request,
            });
        }
        let mut grants = component
            .mutations
            .lock()
            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
        if !component.active.load(Ordering::Acquire) {
            return Err(CapabilityMutationError::NotRegistered { component_id });
        }
        if let Some(current) = grants.get(&request) {
            return Err(CapabilityMutationError::AlreadyGranted {
                current: current.clone(),
            });
        }
        let grant = Grant {
            component_id,
            request: request.clone(),
            provenance,
        };
        grants.insert(request, grant.clone());
        component.grants.store(Arc::new(grants.clone()));
        Ok(grant)
    }

    fn revoke(
        &self,
        component_id: ComponentId,
        request: &CapabilityRequest,
    ) -> Result<RevokeOutcome, CapabilityMutationError> {
        let component = self.component(component_id)?;
        let mut grants = component
            .mutations
            .lock()
            .map_err(|_| CapabilityMutationError::SynchronizationPoisoned)?;
        if !component.active.load(Ordering::Acquire) {
            return Err(CapabilityMutationError::NotRegistered { component_id });
        }
        let removed = grants.remove(request);
        if removed.is_some() {
            component.grants.store(Arc::new(grants.clone()));
        }
        Ok(removed.map_or(RevokeOutcome::NotGranted, RevokeOutcome::Revoked))
    }

    fn grants_for(&self, component_id: ComponentId) -> Result<Vec<Grant>, CapabilityMutationError> {
        let component = self.component(component_id)?;
        if !component.active.load(Ordering::Acquire) {
            return Err(CapabilityMutationError::NotRegistered { component_id });
        }
        let mut grants: Vec<_> = component.grants.load().values().cloned().collect();
        sort_grants(&mut grants);
        Ok(grants)
    }

    fn full_table(&self) -> Vec<Grant> {
        let components = self.components.load();
        let mut grants = Vec::new();
        for component in components.values() {
            if component.active.load(Ordering::Acquire) {
                grants.extend(component.grants.load().values().cloned());
            }
        }
        sort_grants(&mut grants);
        grants
    }
}

fn sort_grants(grants: &mut [Grant]) {
    grants.sort_by(|left, right| {
        left.component_id
            .cmp(&right.component_id)
            .then_with(|| left.request.cmp(&right.request))
    });
}

struct ComponentGrants {
    needs: Vec<CapabilityRequest>,
    active: AtomicBool,
    grants: ArcSwap<HashMap<CapabilityRequest, Grant>>,
    mutations: Mutex<HashMap<CapabilityRequest, Grant>>,
}

/// Check-only retained path used by every host-mediated consuming act.
///
/// This handle deliberately carries no verdict. Every call reads current table
/// state, so retaining the handle across revoke cannot retain authority.
#[derive(Clone)]
pub struct CapabilityChecker {
    component_id: ComponentId,
    component: Arc<ComponentGrants>,
    events: Arc<EventHub>,
}

impl CapabilityChecker {
    /// Returns the component identity whose current authority this checker evaluates.
    #[must_use]
    pub const fn component_id(&self) -> ComponentId {
        self.component_id
    }

    /// Evaluates one consuming act against current grants and events one denial.
    ///
    /// # Errors
    ///
    /// Returns a synchronization error if table or event delivery state was
    /// poisoned. Denial itself is a successful typed [`CheckVerdict::Denied`].
    pub fn check(&self, capability: &Capability) -> Result<CheckVerdict, CapabilityCheckError> {
        let valid = capability.validate().is_ok();
        let declared = valid
            && self
                .component
                .needs
                .iter()
                .any(|need| need.capability.covers(capability));
        let allowed = self.component.active.load(Ordering::Acquire)
            && valid
            && self
                .component
                .grants
                .load()
                .values()
                .any(|grant| grant.request.capability.covers(capability));
        if allowed {
            return Ok(CheckVerdict::Allowed);
        }
        let denial = CapabilityDenied {
            component_id: self.component_id,
            kind: capability.kind(),
            scope: capability.scope(),
            declared,
        };
        self.events
            .publish_denial(denial.clone())
            .map_err(|_| CapabilityCheckError::SynchronizationPoisoned)?;
        Ok(CheckVerdict::Denied(denial))
    }
}

/// The embedding host's only mutation and full-inspection surface.
///
/// This facade borrows the registry-owned table and cannot be serialized or
/// represented as a BEAM term. Component-facing checkers expose no route back.
pub struct HostCapabilityFacade<'a> {
    table: &'a CapabilityTable,
    events: Arc<EventHub>,
}

impl<'a> HostCapabilityFacade<'a> {
    pub(crate) fn new(table: &'a CapabilityTable, events: Arc<EventHub>) -> Self {
        Self { table, events }
    }

    /// Applies one explicit grant that exactly matches a manifest declaration.
    ///
    /// # Errors
    ///
    /// Refuses missing components, undeclared needs, duplicate active grants,
    /// or poisoned synchronization with typed current-state information.
    pub fn grant(
        &self,
        component_id: ComponentId,
        request: CapabilityRequest,
        provenance: GrantProvenance,
    ) -> Result<Grant, CapabilityMutationError> {
        self.table.grant(component_id, request, provenance)
    }

    /// Removes one exact grant, returning a typed idempotent no-op when absent.
    ///
    /// # Errors
    ///
    /// Refuses missing components or poisoned synchronization.
    pub fn revoke(
        &self,
        component_id: ComponentId,
        request: &CapabilityRequest,
    ) -> Result<RevokeOutcome, CapabilityMutationError> {
        self.table.revoke(component_id, request)
    }

    /// Returns active grants for one registered component, including provenance.
    ///
    /// # Errors
    ///
    /// Refuses missing components or poisoned synchronization.
    pub fn grants_for(
        &self,
        component_id: ComponentId,
    ) -> Result<Vec<Grant>, CapabilityMutationError> {
        self.table.grants_for(component_id)
    }

    /// Returns a wait-free snapshot of every active grant row and provenance.
    #[must_use]
    pub fn full_table(&self) -> Vec<Grant> {
        self.table.full_table()
    }

    /// Creates a retained check-only path for a registered component.
    ///
    /// # Errors
    ///
    /// Refuses missing components or poisoned synchronization.
    pub fn checker(
        &self,
        component_id: ComponentId,
    ) -> Result<CapabilityChecker, CapabilityMutationError> {
        Ok(CapabilityChecker {
            component_id,
            component: self.table.component(component_id)?,
            events: Arc::clone(&self.events),
        })
    }
}

/// Linearized result of idempotently revoking one exact grant.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RevokeOutcome {
    /// The active row was removed and returned for audit reconstruction.
    Revoked(Grant),
    /// No active row existed at the mutation point.
    NotGranted,
}

/// A typed current-state refusal from the host grant table.
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum CapabilityMutationError {
    /// The identity has no current registered incarnation.
    #[error("component {component_id} is not registered in the capability table")]
    NotRegistered {
        /// Missing or removed identity.
        component_id: ComponentId,
    },
    /// The requested row was absent from the component manifest.
    #[error("component {component_id} did not declare capability need {request:?}")]
    UndeclaredNeed {
        /// Component that would receive authority.
        component_id: ComponentId,
        /// Exact row refused by the host.
        request: CapabilityRequest,
    },
    /// The exact row already exists; provenance identifies its current state.
    #[error("capability grant already exists: {current:?}")]
    AlreadyGranted {
        /// Active row observed at the mutation point.
        current: Grant,
    },
    /// Grant table synchronization was poisoned by a panic.
    #[error("capability table synchronization is poisoned")]
    SynchronizationPoisoned,
}

/// Failure to produce and publish a fresh check verdict.
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum CapabilityCheckError {
    /// No current incarnation exists for the identity.
    #[error("component {component_id} is not registered for capability checks")]
    NotRegistered {
        /// Missing identity.
        component_id: ComponentId,
    },
    /// Grant table or lifecycle event synchronization was poisoned by a panic.
    #[error("capability check synchronization is poisoned")]
    SynchronizationPoisoned,
}