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}