Skip to main content

coil_runtime/wasm/host/
mod.rs

1use std::fs;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use super::executor::RuntimeHostServiceExecutor;
6use super::support::{http_method_to_wasm, invocation_surface_path};
7use super::*;
8use sha2::{Digest, Sha256};
9use thiserror::Error;
10
11mod cache;
12mod context;
13mod prepare;
14mod principal;
15mod services;
16
17pub use principal::ExtensionPrincipal;
18pub(crate) use services::{MetadataAuditSnapshot, RuntimeWasmHostServices};
19pub use services::{
20    WebhookObservationBackendKind, WebhookObservationEvent, WebhookObservationSnapshot,
21    WebhookObservationStatus, WebhookObservationStatusCounts,
22};
23
24#[derive(Debug, Error)]
25pub enum LiveWasmExecutionError {
26    #[error(transparent)]
27    Model(#[from] WasmModelError),
28    #[error(
29        "failed to read installed extension artifact for `{extension_id}` at `{path}`: {reason}"
30    )]
31    ArtifactRead {
32        extension_id: String,
33        path: String,
34        reason: String,
35    },
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct RegisteredExtensionSlot {
40    pub module: String,
41    pub kind: ExtensionPointKind,
42    pub surface: String,
43    pub description: String,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct InstalledExtensionSummary {
48    pub extension_id: String,
49    pub display_name: String,
50    pub customer_app_id: String,
51    pub handler_count: usize,
52}
53
54#[derive(Debug, Clone)]
55pub struct WasmHost {
56    pub customer_app: String,
57    pub runtime: WasmRuntimeServices,
58    registry: ExtensionRegistry,
59    engine: WasmEngine,
60    tenant_id: i64,
61    default_locale: String,
62    registered_jobs: Vec<RuntimeJobDefinition>,
63    host_services: RuntimeWasmHostServices,
64    host_service_executor: Arc<dyn HostServiceExecutor>,
65    compiled_modules: Arc<cache::CompiledModuleCache<String, CompiledWasmModule>>,
66}
67
68impl WasmHost {
69    pub(crate) fn new(
70        plan: RuntimePlan,
71        customer_app: String,
72        runtime: WasmRuntimeServices,
73        registry: ExtensionRegistry,
74        default_locale: String,
75        registered_jobs: Vec<RuntimeJobDefinition>,
76    ) -> Self {
77        let host_services = RuntimeWasmHostServices::new(plan.clone());
78        let host_service_executor = Arc::new(RuntimeHostServiceExecutor::with_services(
79            plan.clone(),
80            host_services.clone(),
81        ));
82        Self {
83            customer_app,
84            runtime,
85            registry,
86            engine: WasmEngine::new(),
87            tenant_id: plan.tenant_id(),
88            default_locale,
89            registered_jobs,
90            host_services,
91            host_service_executor,
92            compiled_modules: Arc::new(cache::CompiledModuleCache::default()),
93        }
94    }
95
96    pub(crate) fn with_host_services(
97        plan: RuntimePlan,
98        customer_app: String,
99        runtime: WasmRuntimeServices,
100        registry: ExtensionRegistry,
101        default_locale: String,
102        registered_jobs: Vec<RuntimeJobDefinition>,
103        services: RuntimeWasmHostServices,
104    ) -> Self {
105        let host_service_executor = Arc::new(RuntimeHostServiceExecutor::with_services(
106            plan.clone(),
107            services.clone(),
108        ));
109        Self {
110            customer_app,
111            runtime,
112            registry,
113            engine: WasmEngine::new(),
114            tenant_id: plan.tenant_id(),
115            default_locale,
116            registered_jobs,
117            host_services: services,
118            host_service_executor,
119            compiled_modules: Arc::new(cache::CompiledModuleCache::default()),
120        }
121    }
122
123    pub(crate) fn metadata_audit_snapshot(
124        &self,
125        limit: usize,
126    ) -> Result<MetadataAuditSnapshot, String> {
127        self.host_services.metadata_snapshot(limit)
128    }
129
130    pub(crate) fn metadata_audit_backend_kind(&self) -> &'static str {
131        self.host_services.metadata_backend_kind().as_str()
132    }
133
134    pub(crate) fn metadata_audit_location(&self) -> String {
135        self.host_services.metadata_location()
136    }
137
138    pub(crate) fn upsert_customer_managed_asset(
139        &self,
140        logical_path: &str,
141        record_json: &str,
142        updated_at_unix_seconds: i64,
143    ) -> Result<(), String> {
144        self.host_services.upsert_customer_managed_asset(
145            logical_path,
146            record_json,
147            updated_at_unix_seconds,
148        )
149    }
150
151    pub(crate) fn customer_managed_asset(
152        &self,
153        logical_path: &str,
154    ) -> Result<Option<String>, String> {
155        self.host_services.customer_managed_asset(logical_path)
156    }
157
158    pub(crate) fn record_operator_audit(
159        &self,
160        kind: impl Into<String>,
161        app_id: &str,
162        request_id: Option<&str>,
163        principal_id: Option<&str>,
164    ) -> Result<(), String> {
165        self.host_services
166            .record_operator_audit(kind, app_id, request_id, principal_id)
167    }
168
169    pub(crate) fn send_outbound_http(
170        &self,
171        request: &coil_customer_sdk::OutboundHttpRequest,
172    ) -> Result<coil_customer_sdk::OutboundHttpResponse, String> {
173        self.host_services.send_outbound_http(request)
174    }
175
176    pub fn webhook_observation_snapshot(
177        &self,
178        limit: usize,
179    ) -> Result<WebhookObservationSnapshot, String> {
180        self.host_services.webhook_observation_snapshot(limit)
181    }
182
183    pub(crate) fn record_webhook_request_observation(
184        &self,
185        app_id: &str,
186        source: &str,
187        event: &str,
188        status: WebhookObservationStatus,
189        request_id: &str,
190        principal_kind: &str,
191        principal_id: Option<&str>,
192        detail: Option<String>,
193    ) -> Result<(), String> {
194        self.host_services.record_webhook_request_observation(
195            app_id,
196            source,
197            event,
198            status,
199            request_id,
200            principal_kind,
201            principal_id,
202            detail,
203        )
204    }
205
206    pub(crate) fn claim_verified_webhook_delivery(
207        &self,
208        app_id: &str,
209        route_name: &str,
210        source: &str,
211        delivery_id: &str,
212        request_id: &str,
213        recorded_at_unix_seconds: i64,
214    ) -> Result<bool, String> {
215        self.host_services.claim_verified_webhook_delivery(
216            app_id,
217            route_name,
218            source,
219            delivery_id,
220            request_id,
221            recorded_at_unix_seconds,
222        )
223    }
224
225    pub fn compile_module(&self, bytes: &[u8]) -> Result<CompiledWasmModule, WasmModelError> {
226        self.engine.compile_module(bytes)
227    }
228
229    pub fn execute_session(
230        &self,
231        module: &CompiledWasmModule,
232        session: WasmExecutionSession,
233    ) -> Result<ExecutionReceipt, WasmModelError> {
234        let export = self
235            .registry
236            .handler_export(&session.plan().extension_id, &session.plan().handler_id)
237            .ok_or_else(|| WasmModelError::HandlerNotFound {
238                handler_id: session.plan().handler_id.to_string(),
239            })?;
240        self.engine.execute_session(module, session, export)
241    }
242
243    pub fn execute_request_surface(
244        &self,
245        execution: &RequestExecution,
246    ) -> Result<Option<ExecutionReceipt>, LiveWasmExecutionError> {
247        match execution.route_area {
248            RouteArea::Api => self
249                .begin_api_invocation(execution)?
250                .map(|session| self.execute_installed_session(session))
251                .transpose(),
252            _ => self
253                .begin_page_invocation(execution)?
254                .map(|session| self.execute_installed_session(session))
255                .transpose(),
256        }
257    }
258
259    pub fn execute_leased_job(
260        &self,
261        lease: &JobLease,
262    ) -> Result<Option<ExecutionReceipt>, LiveWasmExecutionError> {
263        self.begin_leased_job_invocation(lease)?
264            .map(|session| self.execute_installed_session(session))
265            .transpose()
266    }
267
268    pub fn execute_render_hook_slot(
269        &self,
270        slot: &str,
271        execution: &RequestExecution,
272    ) -> Result<Vec<ExecutionReceipt>, LiveWasmExecutionError> {
273        let sessions = self.begin_render_hook_invocations(slot, execution)?;
274        let mut receipts = Vec::with_capacity(sessions.len());
275        for session in sessions {
276            receipts.push(self.execute_installed_session(session)?);
277        }
278        Ok(receipts)
279    }
280
281    pub fn execute_admin_widget_slot(
282        &self,
283        slot: &str,
284        execution: &RequestExecution,
285    ) -> Result<Vec<ExecutionReceipt>, LiveWasmExecutionError> {
286        let sessions = self.begin_admin_widget_invocations(slot, execution)?;
287        let mut receipts = Vec::with_capacity(sessions.len());
288        for session in sessions {
289            receipts.push(self.execute_installed_session(session)?);
290        }
291        Ok(receipts)
292    }
293
294    fn execute_installed_session(
295        &self,
296        session: WasmExecutionSession,
297    ) -> Result<ExecutionReceipt, LiveWasmExecutionError> {
298        let module = self.load_installed_module(&session.plan().extension_id)?;
299        self.execute_session(module.as_ref(), session)
300            .map_err(Into::into)
301    }
302
303    fn load_installed_module(
304        &self,
305        extension_id: &coil_wasm::ExtensionId,
306    ) -> Result<Arc<CompiledWasmModule>, LiveWasmExecutionError> {
307        let bytes = if let Some(installed) = self.registry.extension(extension_id) {
308            if let Some(artifact) = installed.artifact() {
309                let cache_key = artifact.compiled_module_cache_key().to_string();
310                let bytes = artifact.load_bytes(&self.runtime.extension_directory, extension_id)?;
311                return self.compiled_modules.get_or_insert_with(cache_key, || {
312                    self.compile_module(&bytes).map_err(Into::into)
313                });
314            } else {
315                let path = self.installed_module_path(extension_id);
316                fs::read(&path).map_err(|error| LiveWasmExecutionError::ArtifactRead {
317                    extension_id: extension_id.to_string(),
318                    path: path.display().to_string(),
319                    reason: error.to_string(),
320                })?
321            }
322        } else {
323            let path = self.installed_module_path(extension_id);
324            fs::read(&path).map_err(|error| LiveWasmExecutionError::ArtifactRead {
325                extension_id: extension_id.to_string(),
326                path: path.display().to_string(),
327                reason: error.to_string(),
328            })?
329        };
330
331        let cache_key = format!("{:x}", Sha256::digest(&bytes));
332        self.compiled_modules.get_or_insert_with(cache_key, || {
333            self.compile_module(&bytes).map_err(Into::into)
334        })
335    }
336
337    fn installed_module_path(&self, extension_id: &coil_wasm::ExtensionId) -> PathBuf {
338        PathBuf::from(&self.runtime.extension_directory).join(format!("{extension_id}.wasm"))
339    }
340}