Skip to main content

camel_component_wasm/
lib.rs

1//! WebAssembly component for rust-camel — executes WASM modules as route processors via a Wasmtime runtime.
2//!
3//! Main types: `WasmComponent`, `WasmBundle`, `WasmConfig`, `WasmEndpoint`, `StateStore`.
4//! Main modules: `runtime`, `bindings`, `state_store`, `host_functions`.
5//!
6//! # Limitations
7//!
8//! - Only the [Wasmtime](https://wasmtime.dev) runtime is supported; other WASM runtimes
9//!   (Wasmer, WasmEdge, etc.) are not compatible.
10//! - WASM modules must export a specific interface defined by `camel_wasm_bindings`; arbitrary
11//!   WASM binaries cannot be dropped in without meeting this contract.
12//! - The WASM sandbox has no access to the host filesystem or network by default;
13//!   host functions are limited to what is explicitly exposed via `host_functions`.
14//! - Epoch-based fuel/interruption requires the `epoch` feature; long-running modules
15//!   without epoch support may block the async executor.
16
17pub mod authorization_policy;
18pub mod bean;
19pub mod bean_bindings;
20pub mod bindings;
21pub mod bundle;
22mod cancel_guard;
23pub mod capabilities;
24pub mod config;
25pub mod endpoint;
26pub mod epoch;
27pub mod error;
28pub mod health;
29pub mod host_functions;
30pub(crate) mod metadata;
31pub mod producer;
32mod return_stream;
33pub mod runtime;
34pub mod security_policy;
35pub mod security_policy_bindings;
36pub mod serde_bridge;
37mod source_auth_edge;
38pub mod source_bindings;
39pub mod source_consumer;
40pub mod source_host;
41pub mod staged_listener;
42pub mod state_store;
43pub mod stream_bridge;
44mod wasi_surface;
45pub mod wasm_plugin_context;
46
47pub use authorization_policy::{WasmAuthorizationPolicyEvaluator, build_permission_registry};
48pub use bundle::WasmBundle;
49pub use config::WasmConfig;
50pub use endpoint::WasmEndpoint;
51pub use epoch::EpochTicker;
52pub use error::{TrapReason, WasmError};
53pub use health::WasmHealthCheck;
54pub use security_policy::{WasmSecurityPolicy, build_security_policy_registry};
55pub use state_store::StateStore;
56
57use std::collections::HashMap;
58use std::path::PathBuf;
59use std::sync::Arc;
60use std::sync::OnceLock;
61use std::sync::RwLock;
62use std::sync::atomic::AtomicBool;
63
64use camel_api::CamelError;
65use camel_component_api::{Component, ComponentContext, ComponentMetadata, Endpoint};
66use metadata::WasmMetadataDescriptor;
67
68pub struct WasmComponent {
69    registry: Arc<dyn ComponentContext>,
70    base_dir: PathBuf,
71    engine_loaded: Arc<AtomicBool>,
72}
73
74impl WasmComponent {
75    pub fn new(registry: Arc<dyn ComponentContext>, base_dir: PathBuf) -> Self {
76        Self {
77            registry,
78            base_dir,
79            // TODO: set to true when WASM module is successfully loaded/instantiated
80            engine_loaded: Arc::new(AtomicBool::new(false)),
81        }
82    }
83
84    fn validate_and_resolve_path(&self, uri_path: &str) -> Result<PathBuf, CamelError> {
85        if PathBuf::from(uri_path).is_absolute() {
86            return Err(CamelError::InvalidUri(
87                "WASM path must be relative (not absolute)".to_string(),
88            ));
89        }
90
91        if PathBuf::from(uri_path)
92            .components()
93            .any(|c| matches!(c, std::path::Component::ParentDir))
94        {
95            return Err(CamelError::InvalidUri(
96                "WASM path must not contain '..'".to_string(),
97            ));
98        }
99
100        let resolved = self.base_dir.join(uri_path);
101        let canonical = resolved.canonicalize().map_err(|_| {
102            CamelError::ComponentNotFound(format!("WASM module not found: {}", resolved.display()))
103        })?;
104
105        let canonical_base = self.base_dir.canonicalize().map_err(|_| {
106            CamelError::EndpointCreationFailed(format!(
107                "failed to resolve base directory: {}",
108                self.base_dir.display()
109            ))
110        })?;
111
112        if !canonical.starts_with(&canonical_base) {
113            return Err(CamelError::InvalidUri(
114                "WASM path escapes project root".to_string(),
115            ));
116        }
117
118        Ok(canonical)
119    }
120}
121
122impl Component for WasmComponent {
123    fn scheme(&self) -> &str {
124        "wasm"
125    }
126
127    fn metadata(&self) -> ComponentMetadata {
128        WasmMetadataDescriptor::metadata()
129    }
130
131    fn create_endpoint(
132        &self,
133        uri: &str,
134        ctx: &dyn ComponentContext,
135    ) -> Result<Box<dyn Endpoint>, CamelError> {
136        let uri_without_scheme = uri.strip_prefix("wasm:").ok_or_else(|| {
137            CamelError::InvalidUri(format!("WASM URI must start with 'wasm:': {uri}"))
138        })?;
139
140        let (path_part, wasm_config) = crate::config::WasmConfig::from_uri(uri_without_scheme);
141        if path_part.is_empty() {
142            return Err(CamelError::InvalidUri(
143                "WASM URI must include a module path".to_string(),
144            ));
145        }
146
147        let module_path = self.validate_and_resolve_path(&path_part)?;
148
149        let health_check = WasmHealthCheck::new(Arc::clone(&self.engine_loaded));
150        ctx.register_current_route_health_check(Arc::new(health_check));
151
152        Ok(Box::new(WasmEndpoint::new(
153            uri.to_string(),
154            module_path,
155            self.registry.clone(),
156            wasm_config,
157        )))
158    }
159}
160
161/// Crate-global per-bind public-exposure acknowledgements for `wasm:`
162/// source routes (ADR-0061 Rule 4). Mirrors the mcp registry's bind-ack
163/// store (`McpServerRegistry`, camel-component-mcp/src/registry.rs): the
164/// CLI installs this map from `CamelConfig.binds`
165/// (`allow_public_exposure`) so the bind gate in
166/// `WasmSourceConsumer::start()` fails closed on non-loopback binds until
167/// acknowledged. The `wasm:` gate runs inside the consumer (no shared
168/// listener registry to hang it on), so the store lives here instead.
169pub struct WasmSourceBindAcks {
170    acks: OnceLock<RwLock<HashMap<String, bool>>>,
171}
172
173impl WasmSourceBindAcks {
174    /// Returns the process-global singleton (init-once).
175    pub fn global() -> &'static Self {
176        static INSTANCE: OnceLock<WasmSourceBindAcks> = OnceLock::new();
177        INSTANCE.get_or_init(|| WasmSourceBindAcks {
178            acks: OnceLock::new(),
179        })
180    }
181
182    /// Install per-bind public-exposure acknowledgements. Replaces the
183    /// whole map (interior mutability; no reset hook — tests install a
184    /// fresh map or use distinct ephemeral ports).
185    pub fn set(&self, acks: HashMap<String, bool>) {
186        let lock = self.acks.get_or_init(|| RwLock::new(HashMap::new()));
187        *lock
188            .write()
189            .unwrap_or_else(|poisoned| poisoned.into_inner()) = acks;
190    }
191
192    /// Whether the operator acknowledged public exposure for `bind`
193    /// (bind address string, e.g. `"0.0.0.0:8080"`). Absent or never
194    /// installed → false (fail-closed).
195    pub fn acknowledged(&self, bind: &str) -> bool {
196        let lock = match self.acks.get() {
197            Some(lock) => lock,
198            None => return false,
199        };
200        lock.read()
201            .unwrap_or_else(|poisoned| poisoned.into_inner())
202            .get(bind)
203            .copied()
204            .unwrap_or(false)
205    }
206}