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}