Skip to main content

camel_config/
wasm_limits.rs

1//! Shared WASM runtime limits surfaced through `Camel.toml`.
2//!
3//! Embedded by [`BeanConfig`](crate::config::BeanConfig) and
4//! [`PermissionProviderConfig`](crate::config::PermissionProviderConfig) so that
5//! every WASM consumer (Processor endpoint URI, Bean, AuthorizationPolicy,
6//! SecurityPolicy) accepts the same knobs. Processor continues to parse its
7//! knobs from the `wasm:` URI query string; non-Processor types read this
8//! struct from `Camel.toml`.
9//!
10//! All fields are `Option`; `None` means "use the runtime default" — there is
11//! no silent default lie here (per ADR-0011).
12
13use serde::{Deserialize, Serialize};
14
15/// Tunable WASM runtime limits for a single plugin instance.
16///
17/// Surfaced in `Camel.toml` as:
18///
19/// ```toml
20/// [default.beans.my-bean]
21/// plugin = "my-plugin"
22/// [default.beans.my-bean.limits]
23/// timeout-secs = 600
24/// max-memory = 4294967296   # 4 GiB
25/// max-concurrent-calls = 1
26/// ```
27#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
28#[serde(rename_all = "kebab-case", deny_unknown_fields)]
29pub struct WasmLimitsConfig {
30    /// Maximum execution time per guest call, in seconds.
31    /// Maps to `WasmConfig::timeout_secs` when `Some`.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub timeout_secs: Option<u64>,
34
35    /// Maximum linear memory the guest can allocate, in bytes.
36    /// Maps to `WasmConfig::max_memory_bytes` when `Some`.
37    ///
38    /// Note: when `None`, the runtime default (50 MiB) is used and IS enforced
39    /// via `wasmtime::StoreLimitsBuilder::memory_size`.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub max_memory: Option<u64>,
42
43    /// Maximum concurrent guest invocations against this plugin instance.
44    /// Maps to `WasmConfig::max_concurrent_calls` when `Some`.
45    /// Only meaningful for Processor plugins (Bean/AuthzPolicy/SecurityPolicy
46    /// are not concurrent-safe across reentrant calls today).
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub max_concurrent_calls: Option<usize>,
49
50    /// Maximum .wasm file size in bytes. None = use runtime default (10 MB).
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub max_wasm_size: Option<u64>,
53
54    /// Comma-separated URI schemes the guest may call via camel_call/camel_poll.
55    /// None or empty = deny all (fail-closed). Example: "log,direct,file".
56    /// Ignored for AuthorizationPolicy/SecurityPolicy worlds (always denied).
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub allow_call_schemes: Option<String>,
59
60    /// Maximum bytes for streaming body bridge between host and guest.
61    /// Maps to `WasmConfig::max_stream_bytes` when `Some`.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub max_stream_bytes: Option<u64>,
64
65    /// Maximum core instances per store.
66    /// Maps to `WasmConfig::max_instances` when `Some`.
67    /// Default (when `None`): 10_000 (matches wasmtime default).
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub max_instances: Option<usize>,
70
71    /// Maximum tables per store.
72    /// Maps to `WasmConfig::max_tables` when `Some`.
73    /// Default (when `None`): 10_000 (matches wasmtime default).
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub max_tables: Option<usize>,
76
77    /// Maximum table elements. `None` = no cap (wasmtime unlimited).
78    /// Maps to `WasmConfig::max_table_elements` when `Some`.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub max_table_elements: Option<usize>,
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn defaults_to_all_none() {
89        let cfg = WasmLimitsConfig::default();
90        assert_eq!(cfg.timeout_secs, None);
91        assert_eq!(cfg.max_memory, None);
92        assert_eq!(cfg.max_concurrent_calls, None);
93        assert_eq!(cfg.max_wasm_size, None);
94        assert_eq!(cfg.allow_call_schemes, None);
95        assert_eq!(cfg.max_stream_bytes, None);
96        assert_eq!(cfg.max_instances, None);
97        assert_eq!(cfg.max_tables, None);
98        assert_eq!(cfg.max_table_elements, None);
99    }
100
101    #[test]
102    fn deserialises_full_block() {
103        let toml = toml::toml! {
104            timeout-secs = 600
105            max-memory = 4294967296i64
106            max-concurrent-calls = 2
107            max-wasm-size = 20971520i64
108            allow-call-schemes = "log,direct"
109            max-stream-bytes = 52428800i64
110            max-instances = 100
111            max-tables = 50
112            max-table-elements = 200
113        };
114        let cfg: WasmLimitsConfig = toml.try_into().expect("deserialize");
115        assert_eq!(cfg.timeout_secs, Some(600));
116        assert_eq!(cfg.max_memory, Some(4_294_967_296));
117        assert_eq!(cfg.max_concurrent_calls, Some(2));
118        assert_eq!(cfg.max_wasm_size, Some(20_971_520));
119        assert_eq!(cfg.allow_call_schemes.as_deref(), Some("log,direct"));
120        assert_eq!(cfg.max_stream_bytes, Some(52_428_800));
121        assert_eq!(cfg.max_instances, Some(100));
122        assert_eq!(cfg.max_tables, Some(50));
123        assert_eq!(cfg.max_table_elements, Some(200));
124    }
125
126    #[test]
127    fn deserialises_partial_block() {
128        let toml = toml::toml! {
129            timeout-secs = 120
130        };
131        let cfg: WasmLimitsConfig = toml.try_into().expect("deserialize");
132        assert_eq!(cfg.timeout_secs, Some(120));
133        assert_eq!(cfg.max_memory, None);
134        assert_eq!(cfg.max_concurrent_calls, None);
135        assert_eq!(cfg.max_wasm_size, None);
136        assert_eq!(cfg.allow_call_schemes, None);
137        assert_eq!(cfg.max_stream_bytes, None);
138        assert_eq!(cfg.max_instances, None);
139        assert_eq!(cfg.max_tables, None);
140        assert_eq!(cfg.max_table_elements, None);
141    }
142
143    #[test]
144    fn deserialises_allow_call_schemes() {
145        let toml = toml::toml! {
146            timeout-secs = 10
147            allow-call-schemes = "log,direct"
148        };
149        let cfg: WasmLimitsConfig = toml.try_into().expect("deserialize");
150        assert_eq!(cfg.allow_call_schemes.as_deref(), Some("log,direct"));
151    }
152
153    #[test]
154    fn rejects_unknown_field() {
155        let toml = toml::toml! {
156            timeout-secs = 10
157            fuel = 1000
158        };
159        let result: Result<WasmLimitsConfig, _> = toml.try_into();
160        assert!(result.is_err(), "deny_unknown_fields must reject `fuel`");
161    }
162
163    #[test]
164    fn serde_round_trip_preserves_set_fields() {
165        let original = WasmLimitsConfig {
166            timeout_secs: Some(45),
167            max_memory: Some(64 * 1024 * 1024),
168            max_concurrent_calls: None,
169            ..WasmLimitsConfig::default()
170        };
171        let serialized = toml::to_string(&original).expect("serialize");
172        let back: WasmLimitsConfig = toml::from_str(&serialized).expect("deserialize");
173        assert_eq!(original, back);
174    }
175
176    #[test]
177    fn skip_serializing_none_fields() {
178        let cfg = WasmLimitsConfig {
179            timeout_secs: Some(30),
180            max_memory: None,
181            max_concurrent_calls: None,
182            ..WasmLimitsConfig::default()
183        };
184        let s = toml::to_string(&cfg).expect("serialize");
185        assert!(s.contains("timeout-secs"));
186        assert!(!s.contains("max-memory"));
187        assert!(!s.contains("max-concurrent-calls"));
188        assert!(!s.contains("max-wasm-size"));
189        assert!(!s.contains("allow-call-schemes"));
190        assert!(!s.contains("max-stream-bytes"));
191        assert!(!s.contains("max-instances"));
192        assert!(!s.contains("max-tables"));
193        assert!(!s.contains("max-table-elements"));
194    }
195
196    #[test]
197    fn max_stream_bytes_round_trip() {
198        let cfg = WasmLimitsConfig {
199            max_stream_bytes: Some(52_428_800),
200            ..WasmLimitsConfig::default()
201        };
202        let serialized = toml::to_string(&cfg).expect("serialize");
203        assert!(serialized.contains("max-stream-bytes"));
204        let back: WasmLimitsConfig = toml::from_str(&serialized).expect("deserialize");
205        assert_eq!(back.max_stream_bytes, Some(52_428_800));
206    }
207
208    #[test]
209    fn instances_tables_table_elements_round_trip() {
210        let cfg = WasmLimitsConfig {
211            max_instances: Some(100),
212            max_tables: Some(50),
213            max_table_elements: Some(200),
214            ..WasmLimitsConfig::default()
215        };
216        let serialized = toml::to_string(&cfg).expect("serialize");
217        assert!(serialized.contains("max-instances"));
218        assert!(serialized.contains("max-tables"));
219        assert!(serialized.contains("max-table-elements"));
220        let back: WasmLimitsConfig = toml::from_str(&serialized).expect("deserialize");
221        assert_eq!(back.max_instances, Some(100));
222        assert_eq!(back.max_tables, Some(50));
223        assert_eq!(back.max_table_elements, Some(200));
224    }
225}