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    /// Flush any buffered events to the underlying transport.
104    ///
105    /// Default no-op. Buffering transports (e.g. NATS, whose `publish` only
106    /// queues bytes in the client's write buffer) override this so a caller
107    /// about to exit can guarantee the bytes reached the wire. The admission
108    /// ceiling gate (ADR-0025) relies on this: it emits a signed reject receipt
109    /// and then aborts the process immediately, so without a flush the buffered
110    /// publish would be dropped on exit — the decision made, the fact never on
111    /// the wire.
112    async fn flush(&self) -> Result<(), CellosError> {
113        Ok(())
114    }
115}
116
117/// Resolve secret references for a cell run (L3) — no ambient env.
118#[async_trait]
119pub trait SecretBroker: Send + Sync {
120    async fn resolve(
121        &self,
122        key: &str,
123        cell_id: &str,
124        ttl_seconds: u64,
125    ) -> Result<SecretView, CellosError>;
126
127    /// Called after host teardown so brokers can drop cached material for `cell_id` (M3 residue contract).
128    async fn revoke_for_cell(&self, cell_id: &str) -> Result<(), CellosError>;
129
130    /// Optional hook for brokers that can prepare a cell-scoped upstream lease or token
131    /// before the workload starts and revoke it later in [`SecretBroker::revoke_for_cell`].
132    async fn prepare_runtime_secret_lease(
133        &self,
134        _cell_id: &str,
135        _requests: &[RuntimeSecretLeaseRequest],
136    ) -> Result<(), CellosError> {
137        Err(CellosError::SecretBroker(
138            "runtimeLeasedBroker is not supported by this secret broker".into(),
139        ))
140    }
141
142    /// Optional fetch path used by runtime broker delivery modes.
143    ///
144    /// The default behavior is a fresh resolve on each fetch.
145    async fn fetch_runtime_secret(
146        &self,
147        key: &str,
148        cell_id: &str,
149        ttl_seconds: u64,
150    ) -> Result<SecretView, CellosError> {
151        self.resolve(key, cell_id, ttl_seconds).await
152    }
153
154    /// Returns a broker-generated correlation ID to propagate into lifecycle
155    /// events (SEC-16). Implementations that track exec session IDs or
156    /// rotation event IDs should override this so the broker's identifier
157    /// reaches the compliance summary CloudEvent automatically — even when
158    /// the operator did not supply `spec.correlation.correlationId`.
159    ///
160    /// Default returns `None` (no broker-side correlation).
161    fn broker_correlation_id(&self) -> Option<String> {
162        None
163    }
164}
165
166#[async_trait]
167pub trait ExportSink: Send + Sync {
168    /// Best-effort hint for the target kind this sink writes to. Used for
169    /// target-aware failure events when a push fails before a full receipt exists.
170    fn target_kind(&self) -> Option<ExportReceiptTargetKind> {
171        None
172    }
173
174    /// Best-effort destination hint for observability. Successful pushes should
175    /// return the concrete destination in [`ExportReceipt`].
176    fn destination_hint(&self, _name: &str) -> Option<String> {
177        None
178    }
179
180    /// Push an artifact and return the resulting receipt for observability.
181    async fn push(
182        &self,
183        name: &str,
184        path: &str,
185        metadata: &ExportArtifactMetadata,
186    ) -> Result<ExportReceipt, CellosError>;
187}
188
189/// Brokered local LLM inference — implemented by adapter crates in cellos-server and
190/// cellos-personal. cellos-lite never wires a real implementation; use [`NoopInferenceBroker`]
191/// in tests and anywhere inference is not available.
192///
193/// Every request carries a `cell_id` for attribution and audit. The runtime enforces
194/// `max_tokens` as an authority budget. `model_hint` is advisory.
195#[async_trait]
196pub trait InferenceBroker: Send + Sync {
197    async fn infer(&self, request: &InferenceRequest) -> Result<InferenceResponse, CellosError>;
198}
199
200/// No-op inference broker — for tests and cellos-lite (which never bundles an LLM runtime).
201/// Returns an empty response with zero tokens; callers must handle empty content gracefully.
202pub struct NoopInferenceBroker;
203
204#[async_trait]
205impl InferenceBroker for NoopInferenceBroker {
206    async fn infer(&self, _request: &InferenceRequest) -> Result<InferenceResponse, CellosError> {
207        Ok(InferenceResponse {
208            content: String::new(),
209            model: "noop".to_string(),
210            input_tokens: 0,
211            output_tokens: 0,
212        })
213    }
214}
215
216/// No-op sink for tests and minimal runs without NATS.
217pub struct NoopEventSink;
218
219#[async_trait]
220impl EventSink for NoopEventSink {
221    async fn emit(&self, _event: &CloudEventV1) -> Result<(), CellosError> {
222        Ok(())
223    }
224}
225
226/// No-op export sink for tests and specs without `export.artifacts`.
227pub struct NoopExportSink;
228
229#[async_trait]
230impl ExportSink for NoopExportSink {
231    async fn push(
232        &self,
233        name: &str,
234        _path: &str,
235        _metadata: &ExportArtifactMetadata,
236    ) -> Result<ExportReceipt, CellosError> {
237        Err(CellosError::ExportSink(format!(
238            "export requested for artifact {name:?} but no export sink is configured"
239        )))
240    }
241}