Skip to main content

cellos_core/
ports.rs

1//! Ports (traits) — implemented by host, sinks, and brokers at the composition root.
2
3use std::path::PathBuf;
4
5use async_trait::async_trait;
6
7use crate::error::CellosError;
8use crate::types::{
9    CloudEventV1, ExecutionCellDocument, ExportArtifactMetadata, ExportReceipt,
10    ExportReceiptTargetKind, InferenceRequest, InferenceResponse, SecretView,
11};
12
13/// Opaque handle to a running cell (host-specific).
14#[derive(Debug, Clone)]
15pub struct CellHandle {
16    pub cell_id: String,
17    /// Linux cgroup v2 **leaf** directory when the host backend created it (e.g. under `CELLOS_CGROUP_PARENT`).
18    /// The supervisor writes the `spec.run` child PID to `cgroup.procs` **after** `spawn` (see `cellos-supervisor`).
19    pub cgroup_path: Option<PathBuf>,
20    /// Whether this backend applied nftables network enforcement during `create`.
21    ///
22    /// Backends that own in-VM (or in-namespace) network policy use this to surface
23    /// the signal to the supervisor so a `network_enforcement` CloudEvent can be
24    /// emitted with parity to the host-subprocess path:
25    ///
26    /// - `Some(true)`  — nftables enforcement was applied (e.g. TAP+rules provisioned)
27    /// - `Some(false)` — backend manages networking but enforcement was disabled
28    ///   (e.g. `enable_network: false`); still report for parity
29    /// - `None`        — backend does not own network enforcement; the supervisor's
30    ///   subprocess path will surface the signal via `run_cell_command`
31    pub nft_rules_applied: Option<bool>,
32    /// FC-08 — verified SHA256 hex digest of the kernel image this cell booted.
33    ///
34    /// `Some(hex)` when the backend's pre-boot manifest verification (currently
35    /// only the Firecracker backend) hashed the configured kernel artifact and
36    /// it matched the manifest declaration; `None` for backends that do not
37    /// own a manifest (stub, host-cellos host-subprocess path). The supervisor
38    /// surfaces this on `cell.lifecycle.v1.started` so taudit can answer
39    /// "which kernel bytes did this run boot?" without backend-side state.
40    pub kernel_digest_sha256: Option<String>,
41    /// FC-08 — verified SHA256 hex digest of the rootfs image this cell booted.
42    ///
43    /// Same semantics as [`Self::kernel_digest_sha256`], but for the rootfs
44    /// artifact (always verified by the Firecracker backend when a manifest is
45    /// present).
46    pub rootfs_digest_sha256: Option<String>,
47    /// FC-08 — verified SHA256 hex digest of the firecracker binary that booted
48    /// this cell.
49    ///
50    /// `Some(hex)` only when the manifest declared a `firecracker` role entry
51    /// (it is optional — operators may rely on signed package hashes instead);
52    /// `None` otherwise. Backends without a manifest always emit `None`.
53    pub firecracker_digest_sha256: Option<String>,
54}
55
56/// Teardown report — measured signals for operators and teardown tests (extends as L2 matures).
57#[derive(Debug, Clone, Default)]
58pub struct TeardownReport {
59    pub cell_id: String,
60    /// `true` if this cell was known to the host and removed (false on double-destroy or unknown id).
61    pub destroyed: bool,
62    /// How many cells this host backend still tracks **after** this destroy call (should be 0 for “no peer residue” on a single-cell host).
63    pub peers_tracked_after: usize,
64}
65
66/// Declared runtime-broker secret lease input for brokers that can hold
67/// an upstream lease or token for the lifetime of a cell run.
68#[derive(Debug, Clone)]
69pub struct RuntimeSecretLeaseRequest {
70    pub key: String,
71    pub ttl_seconds: u64,
72}
73
74/// Map [`ExecutionCellDocument`] to host isolation primitives (L2).
75#[async_trait]
76pub trait CellBackend: Send + Sync {
77    async fn create(&self, spec: &ExecutionCellDocument) -> Result<CellHandle, CellosError>;
78    async fn destroy(&self, handle: &CellHandle) -> Result<TeardownReport, CellosError>;
79
80    /// Wait for the cell's workload to complete inside the backend and return
81    /// its exit code.
82    ///
83    /// Returns `Some(Ok(code))` when the backend manages in-VM command execution
84    /// (e.g. via the `cellos-init` + vsock bridge in a Firecracker microVM) and
85    /// the exit code has been received.
86    ///
87    /// Returns `Some(Err(...))` if the backend owns execution but the bridge
88    /// failed (e.g. vsock connection dropped before the exit code arrived).
89    ///
90    /// Returns `None` if this backend does not manage workload execution — the
91    /// supervisor will fall back to launching `spec.run.argv` as a host-side
92    /// subprocess.  This is the default for all backends that do not override it.
93    async fn wait_for_in_vm_exit(&self, _cell_id: &str) -> Option<Result<i32, CellosError>> {
94        None
95    }
96}
97
98/// Emit CloudEvents to NATS/JetStream or other transports (L3).
99#[async_trait]
100pub trait EventSink: Send + Sync {
101    async fn emit(&self, event: &CloudEventV1) -> Result<(), CellosError>;
102}
103
104/// Resolve secret references for a cell run (L3) — no ambient env.
105#[async_trait]
106pub trait SecretBroker: Send + Sync {
107    async fn resolve(
108        &self,
109        key: &str,
110        cell_id: &str,
111        ttl_seconds: u64,
112    ) -> Result<SecretView, CellosError>;
113
114    /// Called after host teardown so brokers can drop cached material for `cell_id` (M3 residue contract).
115    async fn revoke_for_cell(&self, cell_id: &str) -> Result<(), CellosError>;
116
117    /// Optional hook for brokers that can prepare a cell-scoped upstream lease or token
118    /// before the workload starts and revoke it later in [`revoke_for_cell`].
119    async fn prepare_runtime_secret_lease(
120        &self,
121        _cell_id: &str,
122        _requests: &[RuntimeSecretLeaseRequest],
123    ) -> Result<(), CellosError> {
124        Err(CellosError::SecretBroker(
125            "runtimeLeasedBroker is not supported by this secret broker".into(),
126        ))
127    }
128
129    /// Optional fetch path used by runtime broker delivery modes.
130    ///
131    /// The default behavior is a fresh resolve on each fetch.
132    async fn fetch_runtime_secret(
133        &self,
134        key: &str,
135        cell_id: &str,
136        ttl_seconds: u64,
137    ) -> Result<SecretView, CellosError> {
138        self.resolve(key, cell_id, ttl_seconds).await
139    }
140
141    /// Returns a broker-generated correlation ID to propagate into lifecycle
142    /// events (SEC-16). Implementations that track exec session IDs or
143    /// rotation event IDs should override this so the broker's identifier
144    /// reaches the compliance summary CloudEvent automatically — even when
145    /// the operator did not supply `spec.correlation.correlationId`.
146    ///
147    /// Default returns `None` (no broker-side correlation).
148    fn broker_correlation_id(&self) -> Option<String> {
149        None
150    }
151}
152
153#[async_trait]
154pub trait ExportSink: Send + Sync {
155    /// Best-effort hint for the target kind this sink writes to. Used for
156    /// target-aware failure events when a push fails before a full receipt exists.
157    fn target_kind(&self) -> Option<ExportReceiptTargetKind> {
158        None
159    }
160
161    /// Best-effort destination hint for observability. Successful pushes should
162    /// return the concrete destination in [`ExportReceipt`].
163    fn destination_hint(&self, _name: &str) -> Option<String> {
164        None
165    }
166
167    /// Push an artifact and return the resulting receipt for observability.
168    async fn push(
169        &self,
170        name: &str,
171        path: &str,
172        metadata: &ExportArtifactMetadata,
173    ) -> Result<ExportReceipt, CellosError>;
174}
175
176/// Brokered local LLM inference — implemented by adapter crates in cellos-server and
177/// cellos-personal. cellos-lite never wires a real implementation; use [`NoopInferenceBroker`]
178/// in tests and anywhere inference is not available.
179///
180/// Every request carries a `cell_id` for attribution and audit. The runtime enforces
181/// `max_tokens` as an authority budget. `model_hint` is advisory.
182#[async_trait]
183pub trait InferenceBroker: Send + Sync {
184    async fn infer(&self, request: &InferenceRequest) -> Result<InferenceResponse, CellosError>;
185}
186
187/// No-op inference broker — for tests and cellos-lite (which never bundles an LLM runtime).
188/// Returns an empty response with zero tokens; callers must handle empty content gracefully.
189pub struct NoopInferenceBroker;
190
191#[async_trait]
192impl InferenceBroker for NoopInferenceBroker {
193    async fn infer(&self, _request: &InferenceRequest) -> Result<InferenceResponse, CellosError> {
194        Ok(InferenceResponse {
195            content: String::new(),
196            model: "noop".to_string(),
197            input_tokens: 0,
198            output_tokens: 0,
199        })
200    }
201}
202
203/// No-op sink for tests and minimal runs without NATS.
204pub struct NoopEventSink;
205
206#[async_trait]
207impl EventSink for NoopEventSink {
208    async fn emit(&self, _event: &CloudEventV1) -> Result<(), CellosError> {
209        Ok(())
210    }
211}
212
213/// No-op export sink for tests and specs without `export.artifacts`.
214pub struct NoopExportSink;
215
216#[async_trait]
217impl ExportSink for NoopExportSink {
218    async fn push(
219        &self,
220        name: &str,
221        _path: &str,
222        _metadata: &ExportArtifactMetadata,
223    ) -> Result<ExportReceipt, CellosError> {
224        Err(CellosError::ExportSink(format!(
225            "export requested for artifact {name:?} but no export sink is configured"
226        )))
227    }
228}