camel-component-wasm 0.9.0

WASM plugin component for rust-camel
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# camel-component-wasm

WASM plugin component for rust-camel — loads and executes WASM modules as route processors using the Component Model.

## Features

- **WASM Component Model**: Loads WASM modules compiled for `wasm32-wasip2` target
- **Wasmtime v31**: Latest runtime with async support and component-model features
- **Host Functions**: `camel_call()`, `get_property()`, `set_property()`, `host_store()`, `host_load()` for guest-host communication
- **URI-Based Routing**: `wasm:path/to/module.wasm` format for easy integration
- **Path Validation**: Prevents directory traversal and escapes from project root
- **Recursion Guard**: Blocks nested WASM calls to prevent infinite loops
- **Tower Service**: Implements `Service<Exchange>` for async processing
- **Exchange Properties**: Per-request properties accessible from WASM via host functions
- **Persistent State**: `host_store`/`host_load` for per-endpoint state that survives across `process()` calls
- **Production Hardening**: Epoch-based timeouts, memory limits, structured trap classification, automatic recovery

## Installation

Add to `Cargo.toml` (workspace dependency):

```toml
[dependencies]
camel-component-wasm.workspace = true
```

## URI Format

```
wasm:path/to/module.wasm[?timeout=<secs>&max-memory=<bytes>]
```

- Must be relative path (no leading `/`)
- No `..` components allowed
- Resolved against configured base directory

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `timeout` | `u64` (seconds) | `30` | Max wall-clock time per guest call. Enforced via epoch interruption. |
| `max-memory` | `u64` (bytes) | `52428800` (50 MB) | Max linear memory the guest can allocate. |

Zero or invalid values are silently ignored and the default is used.

### Examples

```
wasm:plugins/transform.wasm
wasm:plugins/transform.wasm?timeout=5
wasm:plugins/transform.wasm?timeout=10&max-memory=10485760
```

## Host Functions

WASM plugins can call these host functions from guest code:

### `camel_call(uri: String, payload: String) -> Result<String>`

Calls another endpoint from within WASM.

```rust
// Guest-side (WASM)
use camel_wasm_sdk::Host;

async fn process(exchange: &mut Exchange) -> Result<()> {
    let response = Host::camel_call("log:info", "Processing data".to_string()).await?;
    Ok(())
}
```

### `get_property(key: String) -> Option<String>`

Retrieves an exchange property by key.

```rust
let user_id = Host::get_property("userId".to_string()).await?;
```

### `set_property(key: String, value: String)`

Sets an exchange property. Value can be JSON string for structured data.

```rust
Host::set_property("timestamp".to_string(), "2024-01-01T00:00:00Z".to_string()).await;
Host::set_property("metadata".to_string(), r#"{"source":"wasm"}"#.to_string()).await;
```

### `host_store(key: String, value: String) -> Result<()>`

Stores a key-value pair that persists across `process()` calls for this route endpoint.

```rust
use camel_wasm_sdk::state_helpers;

// Store config loaded in init()
state_helpers::store("api-key", "secret-123")?;

// Store structured data as JSON
state_helpers::store_json("config", &my_config)?;
```

### `host_load(key: String) -> Result<Option<String>>`

Loads a previously stored value. Returns `None` if the key has not been stored.

```rust
// Load a string value
let api_key = state_helpers::load("api-key")?;

// Load and deserialize JSON
let config: Option<MyConfig> = state_helpers::load_json("config")?;
```

> **Scope:** State is scoped per route endpoint. Different routes using the same `.wasm` file maintain independent state stores.

## Usage

### Registration

```rust
use camel_component_wasm::WasmBundle;
use camel_core::CamelContext;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut ctx = CamelContext::new();
    let registry = ctx.registry().clone();

    // Register WASM component with base directory
    let bundle = WasmBundle::new(registry, PathBuf::from("./wasm-plugins"));
    bundle.register_all(&mut ctx);

    ctx.start().await?;
    Ok(())
}
```

### Route with WASM Processor

```rust
use camel_builder::RouteBuilder;

// Route that processes data through a WASM module
ctx.add_route(
    RouteBuilder::from("http://0.0.0.0:8080/process")
        .to("wasm:plugins/transform.wasm")
        .to("log:info")
        .build()?
).await?;
```

### WASM Plugin Example

Build your plugin with `wasm32-wasip2` target:

```rust
// src/main.rs (guest plugin)
use camel_wasm_sdk::{Exchange, Host, plugin};

#[plugin]
async fn init() -> Result<()> {
    println!("Plugin initialized");
    Ok(())
}

#[plugin]
async fn process(mut exchange: Exchange) -> Result<Exchange> {
    let body = exchange.body.as_text().unwrap_or("");
    let processed = format!("[WASM] {}", body);
    exchange.body = camel_wasm_sdk::Body::Text(processed);

    // Log via host function
    let _ = Host::camel_call("log:debug", format!("Processed: {}", body).into()).await;

    Ok(exchange)
}
```

Build:

```bash
cargo build --target wasm32-wasip2 --release
cp target/wasm32-wasip2/release/plugin.wasm wasm-plugins/
```

### Chaining WASM Plugins

```rust
RouteBuilder::from("direct:chain")
    .to("wasm:plugins/validate.wasm")
    .to("wasm:plugins/transform.wasm")
    .to("wasm:plugins/enrich.wasm")
    .to("log:info")
    .build()?;
```

## Security

### Path Validation

- Absolute paths are rejected
- Paths containing `..` are rejected
- Canonical path must start with base directory
- Prevents directory traversal attacks

### Recursion Guard

- WASM plugins cannot call other WASM plugins via `camel_call()`
- Prevents infinite recursion and stack overflow
- Returns error: `recursive wasm calls not supported`

## Production Configuration

Phase 4 hardening adds epoch-based timeouts, memory limits, and structured trap classification to every plugin call.

### Timeout enforcement

Every guest call (`init` and `process`) sets an epoch deadline before invocation. A background thread (`EpochTicker`) increments the wasmtime engine epoch every 10 ms. If the deadline is exceeded, the call is interrupted and returns `WasmError::Timeout`.

```rust
// 5-second timeout
let uri = "wasm:plugins/slow.wasm?timeout=5";
```

### Memory limits

`StoreLimits` is installed in every `Store`. If the guest exceeds `max-memory`, the next allocation fails and returns `WasmError::OutOfMemory`.

```rust
// 10 MB limit
let uri = "wasm:plugins/heavy.wasm?max-memory=10485760";
```

### Error variants

| Variant | When raised |
|---------|-------------|
| `WasmError::Timeout { plugin, timeout_secs }` | Epoch deadline exceeded |
| `WasmError::OutOfMemory { plugin, max_memory_bytes }` | Guest exceeded memory limit |
| `WasmError::Trap { plugin, reason }` | Guest hit unreachable/stack-overflow/other trap |
| `WasmError::GuestPanic(msg)` | Guest panicked with a message |
| `WasmError::Unhealthy(msg)` | Plugin failed health check |

### Recovery

After a `Timeout`, `Trap`, or `OutOfMemory`, the plugin runtime is automatically reset on the next call. No manual intervention required.

## Architecture

```
┌─────────────────┐
│  WasmComponent  │
│  (scheme: wasm) │
└────────┬────────┘
         │ creates_endpoint()
┌─────────────────┐
│  WasmEndpoint   │
│  (URI resolver) │
└────────┬────────┘
         │ create_producer()
┌─────────────────┐
│  WasmProducer   │
│  (Tower Service)│
└────────┬────────┘
         │ poll_ready() -> call()
┌─────────────────┐
│  WasmRuntime    │
│  (Wasmtime)     │
└─────────────────┘
         │ call_init_once() / call_process()
┌─────────────────┐
│  WasmHostState  │
│  (registry,     │
│   properties,   │
│   state_store,  │
│   call_depth)   │
└─────────────────┘
```

- **WasmComponent**: Component trait implementation, URI scheme `wasm:`, path validation
- **WasmEndpoint**: Resolves URI to WASM module path, creates producer
- **WasmProducer**: Tower Service wrapping WasmRuntime, lazy initialization, error handling
- **WasmRuntime**: Wasmtime engine, linker, component instantiation
- **WasmHostState**: Per-request state with registry, properties, call depth guard

## Host Function Internals

The `WasmHostState` maintains per-invocation state:

```rust
pub struct WasmHostState {
    pub table: ResourceTable,           // Wasmtime resource table
    pub wasi: WasiCtx,                  // WASI system calls
    pub properties: HashMap<String, Value>, // Exchange properties
    pub registry: Arc<Mutex<Registry>>, // Component registry
    pub call_depth: u32,                // Recursion guard (0 = allowed)
    pub state_store: StateStore,        // Per-endpoint persistent state
    pub limits: StoreLimits,            // Memory allocation limits
}
```

Each request gets a new `WasmHostState` with:
- Exchange properties copied from the incoming `Exchange`
- `call_depth` reset to 0
- Fresh WASI context with stderr inheritance

## Testing

Unit tests verify path validation, recursion guard, host functions, state persistence, hardening (epoch timeout, memory limits, trap recovery), and performance benchmarks:

```bash
cargo test -p camel-component-wasm
# 81 tests: 50 unit + 10 hardening + 14 integration + 6 state + 1 perf
```

Integration tests require a compiled WASM module:

```bash
# Build test plugin
cargo build --target wasm32-wasip2 --release -p example-wasm-plugin
cp target/wasm32-wasip2/release/example_wasm_plugin.wasm tests/fixtures/

# Run integration tests
cargo test -p camel-component-wasm --test integration
```

## Guest SDK

See `crates/camel-wasm-sdk/README.md` for plugin development:

- `#[plugin]` macro for exported functions
- `Exchange` and `Body` types for data access
- `Host` trait for calling host functions

## Bean Support

The WASM component also supports **bean plugins** — multi-method WASM components that expose several callable methods from a single module.

### `WasmBean` Adapter

`WasmBean` is the host-side adapter that loads a WASM bean module and dispatches method calls to the correct guest function. It uses the `bean` WIT world (distinct from the `processor` world) to communicate with the guest.

### Configuration

Register beans in `Camel.toml`:

```toml
[beans.auth]
plugin = "my-auth-bean"
```

Each bean entry creates an isolated WASM instance. Methods are invoked by name from YAML DSL or Rust routes:

```yaml
routes:
  - id: "auth-route"
    from: "direct:auth"
    steps:
      - bean:
          name: "auth"
          method: "validate"
```

### Building Bean Plugins

Use the SDK's `BeanPlugin` trait and `export_bean!` macro:

```rust
use camel_wasm_sdk::{export_bean, BeanPlugin, WasmExchange, WasmError};

struct AuthBean;

impl BeanPlugin for AuthBean {
    fn methods() -> Vec<&'static str> {
        vec!["validate", "refresh"]
    }

    fn invoke(method: &str, exchange: WasmExchange) -> Result<WasmExchange, WasmError> {
        match method {
            "validate" => Ok(exchange),
            "refresh" => Ok(exchange),
            _ => Err(WasmError::ProcessorError(format!("unknown method: {method}"))),
        }
    }
}

export_bean!(AuthBean);
```

## Documentation

- [API Documentation]https://docs.rs/camel-component-wasm
- [Repository]https://github.com/kennycallado/rust-camel

## License

This project is licensed under the same license as rust-camel.

## Contributing

See the [main repository](https://github.com/kennycallado/rust-camel) for contribution guidelines.