clawft_kernel/workload_runtime/
logical.rs1use std::collections::HashMap;
11use std::path::PathBuf;
12use std::sync::Arc;
13use std::time::Duration;
14
15use async_trait::async_trait;
16use clawft_types::placement::{AttrValue, Capability, CapabilityId, Provenance};
17use tokio::sync::Mutex;
18
19use super::evidence::RunEvidence;
20use super::types::{
21 Admission, ControlMode, InstanceHandle, InstanceState, InstanceStatus, RuntimeError,
22 VerifiedWorkload, WorkloadConfig, WorkloadRuntime, WorkloadSource,
23};
24use crate::workload_governance::NetworkPolicy;
25
26pub const LOGICAL_ID: &str = "logical";
28pub const CAP_PROJECT_LOGICAL: &str = "runtime.project.logical";
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ChildSpec {
34 pub adapter: String,
36 pub project_id: String,
38 pub root: PathBuf,
40 pub key_id: String,
42 pub cert_serial: u64,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ChildRef {
49 pub project_id: String,
51 pub identity: ChildIdentity,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum ChildIdentity {
57 Native { host_pid: u32 },
58 Container { engine: String, immutable_container_id: String, host_pid: u32 },
59}
60
61impl ChildIdentity {
62 pub fn host_pid(&self) -> u32 {
63 match self { Self::Native { host_pid } | Self::Container { host_pid, .. } => *host_pid }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum ChildProbe {
70 Running {
72 identity: ChildIdentity,
73 },
74 Exited {
76 code: Option<i32>,
78 signal: Option<i32>,
80 },
81 Unverifiable { reason: String },
84 NotStarted,
86}
87
88#[async_trait]
90pub trait ChildLauncher: Send + Sync {
91 async fn spawn(&self, spec: &ChildSpec) -> Result<ChildRef, RuntimeError>;
94 async fn terminate(&self, child: &ChildRef, grace: Duration)
97 -> Result<Option<i32>, RuntimeError>;
98 async fn probe(&self, project_id: &str) -> ChildProbe;
100}
101
102struct Instance {
103 spec: ChildSpec,
104 child: Option<ChildRef>,
105}
106
107pub struct LogicalRuntime {
109 adapter: &'static str,
110 capability: &'static str,
111 launcher: Arc<dyn ChildLauncher>,
112 instances: Mutex<HashMap<String, Instance>>,
113}
114
115impl LogicalRuntime {
116 pub fn wasmtime_project(launcher: Arc<dyn ChildLauncher>) -> Self {
120 Self {
121 adapter: "wasmtime-project-v1",
122 capability: "runtime.project.wasmtime",
123 launcher,
124 instances: Mutex::new(HashMap::new()),
125 }
126 }
127
128 pub fn new(launcher: Arc<dyn ChildLauncher>) -> Self {
130 Self {
131 adapter: LOGICAL_ID,
132 capability: CAP_PROJECT_LOGICAL,
133 launcher,
134 instances: Mutex::new(HashMap::new()),
135 }
136 }
137}
138
139fn instance_id(project_id: &str) -> String {
140 format!("project-{project_id}")
141}
142
143fn spec_of(w: &VerifiedWorkload) -> Result<ChildSpec, RuntimeError> {
144 match &w.source {
145 WorkloadSource::Project(p) => Ok(ChildSpec {
146 adapter: p.adapter.clone(),
147 project_id: p.project_id.clone(),
148 root: p.root.clone(),
149 key_id: p.key_id.clone(),
150 cert_serial: p.cert_serial,
151 }),
152 _ => Err(RuntimeError::AdmissionRefused(format!(
153 "{LOGICAL_ID} runs only project workloads"
154 ))),
155 }
156}
157
158#[async_trait]
159impl WorkloadRuntime for LogicalRuntime {
160 fn id(&self) -> &str {
161 self.adapter
162 }
163
164 fn provides(&self) -> Vec<Capability> {
165 let Ok(id) = CapabilityId::new(self.capability) else {
166 return Vec::new();
167 };
168 vec![Capability::new(id, Provenance::Probed).with_attr("os", AttrValue::from(std::env::consts::OS))]
169 }
170
171 async fn admit(&self, w: &VerifiedWorkload) -> Result<Admission, RuntimeError> {
172 let spec = spec_of(w)?;
173 if spec.adapter != self.adapter {
174 return Err(RuntimeError::AdmissionRefused("project adapter mismatch".into()));
175 }
176 if !spec.root.is_dir() {
177 return Err(RuntimeError::AdmissionRefused(format!(
178 "project root {} is gone",
179 spec.root.display()
180 )));
181 }
182 Ok(Admission {
183 runtime: self.adapter.into(),
184 arch: if self.adapter == LOGICAL_ID {
185 std::env::consts::ARCH
186 } else {
187 "wasm32-wasip1"
188 }
189 .into(),
190 emulated: false,
191 notes: vec![format!("project {} root {}", spec.project_id, spec.root.display())],
192 })
193 }
194
195 async fn load(
196 &self,
197 w: &VerifiedWorkload,
198 _cfg: &WorkloadConfig,
199 ) -> Result<InstanceHandle, RuntimeError> {
200 self.admit(w).await?;
201 let spec = spec_of(w)?;
202 let iid = instance_id(&spec.project_id);
203 let mut map = self.instances.lock().await;
204 if map.contains_key(&iid) {
205 return Err(RuntimeError::InvalidState(format!("{iid} is already loaded")));
206 }
207 let workload_id = spec.project_id.clone();
208 map.insert(iid.clone(), Instance { spec, child: None });
209 Ok(InstanceHandle {
210 runtime: self.adapter.into(),
211 instance_id: iid,
212 workload_id,
213 store_installed: false,
214 })
215 }
216
217 async fn start(&self, h: &InstanceHandle) -> Result<(), RuntimeError> {
218 let mut map = self.instances.lock().await;
219 let inst = map
220 .get_mut(&h.instance_id)
221 .ok_or_else(|| RuntimeError::UnknownInstance(h.instance_id.clone()))?;
222 if matches!(
223 self.launcher.probe(&inst.spec.project_id).await,
224 ChildProbe::Running { .. } | ChildProbe::Unverifiable { .. }
225 ) {
226 return Err(RuntimeError::InvalidState("already running".into()));
227 }
228 if !inst.spec.root.is_dir() {
229 return Err(RuntimeError::AdmissionRefused(format!(
230 "project root {} is gone",
231 inst.spec.root.display()
232 )));
233 }
234 inst.child = Some(self.launcher.spawn(&inst.spec).await?);
235 Ok(())
236 }
237
238 async fn stop(&self, h: &InstanceHandle, grace: Duration) -> Result<RunEvidence, RuntimeError> {
239 let mut map = self.instances.lock().await;
240 let inst = map
241 .get_mut(&h.instance_id)
242 .ok_or_else(|| RuntimeError::UnknownInstance(h.instance_id.clone()))?;
243 let started = std::time::Instant::now();
244 let mut ev = RunEvidence {
245 runtime: self.adapter.into(),
246 instance_id: h.instance_id.clone(),
247 ..RunEvidence::default()
248 };
249 let child = match inst.child.take() {
252 Some(c) => Some(c),
253 None => match self.launcher.probe(&inst.spec.project_id).await {
254 ChildProbe::Running { identity } => Some(ChildRef {
255 project_id: inst.spec.project_id.clone(),
256 identity,
257 }),
258 ChildProbe::Unverifiable { reason } => {
259 return Err(RuntimeError::InvalidState(format!("child unverifiable: {reason}")));
260 }
261 _ => None,
262 },
263 };
264 if let Some(child) = child {
265 ev.exit_code = self.launcher.terminate(&child, grace).await?;
266 }
267 ev.elapsed_ms = started.elapsed().as_millis() as u64;
268 Ok(ev)
269 }
270
271 async fn unload(&self, h: InstanceHandle) -> Result<(), RuntimeError> {
272 let mut map = self.instances.lock().await;
273 let inst = map
274 .get(&h.instance_id)
275 .ok_or_else(|| RuntimeError::UnknownInstance(h.instance_id.clone()))?;
276 if matches!(
277 self.launcher.probe(&inst.spec.project_id).await,
278 ChildProbe::Running { .. } | ChildProbe::Unverifiable { .. }
279 ) {
280 return Err(RuntimeError::InvalidState("stop the child before unloading".into()));
281 }
282 map.remove(&h.instance_id);
283 Ok(())
284 }
285
286 async fn status(&self, h: &InstanceHandle) -> InstanceStatus {
287 let map = self.instances.lock().await;
288 let Some(inst) = map.get(&h.instance_id) else {
289 return InstanceStatus::of(InstanceState::Unknown);
290 };
291 match self.launcher.probe(&inst.spec.project_id).await {
292 ChildProbe::Running { identity } => InstanceStatus {
293 state: InstanceState::Running,
294 exit_code: None,
295 detail: Some(match identity {
296 ChildIdentity::Native { host_pid } => format!("pid {host_pid}"),
297 ChildIdentity::Container { engine, immutable_container_id, .. } => format!("{engine} container {immutable_container_id}"),
298 }),
299 },
300 ChildProbe::Exited { code, signal } => InstanceStatus {
301 state: InstanceState::Exited,
302 exit_code: code,
303 detail: signal.map(|s| format!("signal {s}")),
304 },
305 ChildProbe::Unverifiable { reason } => InstanceStatus {
306 state: InstanceState::Unknown,
307 exit_code: None,
308 detail: Some(reason),
309 },
310 ChildProbe::NotStarted => InstanceStatus::of(InstanceState::Loaded),
311 }
312 }
313
314 fn control_mode(&self) -> ControlMode {
315 ControlMode::Managed
316 }
317
318 fn network_exposure(&self) -> NetworkPolicy {
319 NetworkPolicy::None
320 }
321
322 async fn console(&self, _h: &InstanceHandle, _command: &str) -> Result<RunEvidence, RuntimeError> {
323 Err(RuntimeError::Unsupported("a project kernel has no console cycle".into()))
324 }
325}