cellos-core 0.7.3

CellOS domain types and ports — typed authority, formation DAG, CloudEvent envelopes, RBAC primitives. No I/O.
Documentation
//! Ports (traits) — implemented by host, sinks, and brokers at the composition root.

use std::path::PathBuf;

use async_trait::async_trait;

use crate::error::CellosError;
use crate::types::{
    CloudEventV1, ExecutionCellDocument, ExportArtifactMetadata, ExportReceipt,
    ExportReceiptTargetKind, InferenceRequest, InferenceResponse, SecretView,
};

/// Opaque handle to a running cell (host-specific).
#[derive(Debug, Clone)]
pub struct CellHandle {
    pub cell_id: String,
    /// Linux cgroup v2 **leaf** directory when the host backend created it (e.g. under `CELLOS_CGROUP_PARENT`).
    /// The supervisor writes the `spec.run` child PID to `cgroup.procs` **after** `spawn` (see `cellos-supervisor`).
    pub cgroup_path: Option<PathBuf>,
    /// Whether this backend applied nftables network enforcement during `create`.
    ///
    /// Backends that own in-VM (or in-namespace) network policy use this to surface
    /// the signal to the supervisor so a `network_enforcement` CloudEvent can be
    /// emitted with parity to the host-subprocess path:
    ///
    /// - `Some(true)`  — nftables enforcement was applied (e.g. TAP+rules provisioned)
    /// - `Some(false)` — backend manages networking but enforcement was disabled
    ///   (e.g. `enable_network: false`); still report for parity
    /// - `None`        — backend does not own network enforcement; the supervisor's
    ///   subprocess path will surface the signal via `run_cell_command`
    pub nft_rules_applied: Option<bool>,
    /// FC-08 — verified SHA256 hex digest of the kernel image this cell booted.
    ///
    /// `Some(hex)` when the backend's pre-boot manifest verification (currently
    /// only the Firecracker backend) hashed the configured kernel artifact and
    /// it matched the manifest declaration; `None` for backends that do not
    /// own a manifest (stub, host-cellos host-subprocess path). The supervisor
    /// surfaces this on `cell.lifecycle.v1.started` so taudit can answer
    /// "which kernel bytes did this run boot?" without backend-side state.
    pub kernel_digest_sha256: Option<String>,
    /// FC-08 — verified SHA256 hex digest of the rootfs image this cell booted.
    ///
    /// Same semantics as [`Self::kernel_digest_sha256`], but for the rootfs
    /// artifact (always verified by the Firecracker backend when a manifest is
    /// present).
    pub rootfs_digest_sha256: Option<String>,
    /// FC-08 — verified SHA256 hex digest of the firecracker binary that booted
    /// this cell.
    ///
    /// `Some(hex)` only when the manifest declared a `firecracker` role entry
    /// (it is optional — operators may rely on signed package hashes instead);
    /// `None` otherwise. Backends without a manifest always emit `None`.
    pub firecracker_digest_sha256: Option<String>,
}

/// Teardown report — measured signals for operators and teardown tests (extends as L2 matures).
#[derive(Debug, Clone, Default)]
pub struct TeardownReport {
    pub cell_id: String,
    /// `true` if this cell was known to the host and removed (false on double-destroy or unknown id).
    pub destroyed: bool,
    /// How many cells this host backend still tracks **after** this destroy call (should be 0 for “no peer residue” on a single-cell host).
    pub peers_tracked_after: usize,
}

/// Declared runtime-broker secret lease input for brokers that can hold
/// an upstream lease or token for the lifetime of a cell run.
#[derive(Debug, Clone)]
pub struct RuntimeSecretLeaseRequest {
    pub key: String,
    pub ttl_seconds: u64,
}

/// Map [`ExecutionCellDocument`] to host isolation primitives (L2).
#[async_trait]
pub trait CellBackend: Send + Sync {
    async fn create(&self, spec: &ExecutionCellDocument) -> Result<CellHandle, CellosError>;
    async fn destroy(&self, handle: &CellHandle) -> Result<TeardownReport, CellosError>;

    /// Wait for the cell's workload to complete inside the backend and return
    /// its exit code.
    ///
    /// Returns `Some(Ok(code))` when the backend manages in-VM command execution
    /// (e.g. via the `cellos-init` + vsock bridge in a Firecracker microVM) and
    /// the exit code has been received.
    ///
    /// Returns `Some(Err(...))` if the backend owns execution but the bridge
    /// failed (e.g. vsock connection dropped before the exit code arrived).
    ///
    /// Returns `None` if this backend does not manage workload execution — the
    /// supervisor will fall back to launching `spec.run.argv` as a host-side
    /// subprocess.  This is the default for all backends that do not override it.
    async fn wait_for_in_vm_exit(&self, _cell_id: &str) -> Option<Result<i32, CellosError>> {
        None
    }
}

/// Emit CloudEvents to NATS/JetStream or other transports (L3).
#[async_trait]
pub trait EventSink: Send + Sync {
    async fn emit(&self, event: &CloudEventV1) -> Result<(), CellosError>;
}

/// Resolve secret references for a cell run (L3) — no ambient env.
#[async_trait]
pub trait SecretBroker: Send + Sync {
    async fn resolve(
        &self,
        key: &str,
        cell_id: &str,
        ttl_seconds: u64,
    ) -> Result<SecretView, CellosError>;

    /// Called after host teardown so brokers can drop cached material for `cell_id` (M3 residue contract).
    async fn revoke_for_cell(&self, cell_id: &str) -> Result<(), CellosError>;

    /// Optional hook for brokers that can prepare a cell-scoped upstream lease or token
    /// before the workload starts and revoke it later in [`revoke_for_cell`].
    async fn prepare_runtime_secret_lease(
        &self,
        _cell_id: &str,
        _requests: &[RuntimeSecretLeaseRequest],
    ) -> Result<(), CellosError> {
        Err(CellosError::SecretBroker(
            "runtimeLeasedBroker is not supported by this secret broker".into(),
        ))
    }

    /// Optional fetch path used by runtime broker delivery modes.
    ///
    /// The default behavior is a fresh resolve on each fetch.
    async fn fetch_runtime_secret(
        &self,
        key: &str,
        cell_id: &str,
        ttl_seconds: u64,
    ) -> Result<SecretView, CellosError> {
        self.resolve(key, cell_id, ttl_seconds).await
    }

    /// Returns a broker-generated correlation ID to propagate into lifecycle
    /// events (SEC-16). Implementations that track exec session IDs or
    /// rotation event IDs should override this so the broker's identifier
    /// reaches the compliance summary CloudEvent automatically — even when
    /// the operator did not supply `spec.correlation.correlationId`.
    ///
    /// Default returns `None` (no broker-side correlation).
    fn broker_correlation_id(&self) -> Option<String> {
        None
    }
}

#[async_trait]
pub trait ExportSink: Send + Sync {
    /// Best-effort hint for the target kind this sink writes to. Used for
    /// target-aware failure events when a push fails before a full receipt exists.
    fn target_kind(&self) -> Option<ExportReceiptTargetKind> {
        None
    }

    /// Best-effort destination hint for observability. Successful pushes should
    /// return the concrete destination in [`ExportReceipt`].
    fn destination_hint(&self, _name: &str) -> Option<String> {
        None
    }

    /// Push an artifact and return the resulting receipt for observability.
    async fn push(
        &self,
        name: &str,
        path: &str,
        metadata: &ExportArtifactMetadata,
    ) -> Result<ExportReceipt, CellosError>;
}

/// Brokered local LLM inference — implemented by adapter crates in cellos-server and
/// cellos-personal. cellos-lite never wires a real implementation; use [`NoopInferenceBroker`]
/// in tests and anywhere inference is not available.
///
/// Every request carries a `cell_id` for attribution and audit. The runtime enforces
/// `max_tokens` as an authority budget. `model_hint` is advisory.
#[async_trait]
pub trait InferenceBroker: Send + Sync {
    async fn infer(&self, request: &InferenceRequest) -> Result<InferenceResponse, CellosError>;
}

/// No-op inference broker — for tests and cellos-lite (which never bundles an LLM runtime).
/// Returns an empty response with zero tokens; callers must handle empty content gracefully.
pub struct NoopInferenceBroker;

#[async_trait]
impl InferenceBroker for NoopInferenceBroker {
    async fn infer(&self, _request: &InferenceRequest) -> Result<InferenceResponse, CellosError> {
        Ok(InferenceResponse {
            content: String::new(),
            model: "noop".to_string(),
            input_tokens: 0,
            output_tokens: 0,
        })
    }
}

/// No-op sink for tests and minimal runs without NATS.
pub struct NoopEventSink;

#[async_trait]
impl EventSink for NoopEventSink {
    async fn emit(&self, _event: &CloudEventV1) -> Result<(), CellosError> {
        Ok(())
    }
}

/// No-op export sink for tests and specs without `export.artifacts`.
pub struct NoopExportSink;

#[async_trait]
impl ExportSink for NoopExportSink {
    async fn push(
        &self,
        name: &str,
        _path: &str,
        _metadata: &ExportArtifactMetadata,
    ) -> Result<ExportReceipt, CellosError> {
        Err(CellosError::ExportSink(format!(
            "export requested for artifact {name:?} but no export sink is configured"
        )))
    }
}