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}