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
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn defaults_to_all_none() {
67        let cfg = WasmLimitsConfig::default();
68        assert_eq!(cfg.timeout_secs, None);
69        assert_eq!(cfg.max_memory, None);
70        assert_eq!(cfg.max_concurrent_calls, None);
71        assert_eq!(cfg.max_wasm_size, None);
72        assert_eq!(cfg.allow_call_schemes, None);
73    }
74
75    #[test]
76    fn deserialises_full_block() {
77        let toml = toml::toml! {
78            timeout-secs = 600
79            max-memory = 4294967296i64
80            max-concurrent-calls = 2
81            max-wasm-size = 20971520i64
82            allow-call-schemes = "log,direct"
83        };
84        let cfg: WasmLimitsConfig = toml.try_into().expect("deserialize");
85        assert_eq!(cfg.timeout_secs, Some(600));
86        assert_eq!(cfg.max_memory, Some(4_294_967_296));
87        assert_eq!(cfg.max_concurrent_calls, Some(2));
88        assert_eq!(cfg.max_wasm_size, Some(20_971_520));
89        assert_eq!(cfg.allow_call_schemes.as_deref(), Some("log,direct"));
90    }
91
92    #[test]
93    fn deserialises_partial_block() {
94        let toml = toml::toml! {
95            timeout-secs = 120
96        };
97        let cfg: WasmLimitsConfig = toml.try_into().expect("deserialize");
98        assert_eq!(cfg.timeout_secs, Some(120));
99        assert_eq!(cfg.max_memory, None);
100        assert_eq!(cfg.max_concurrent_calls, None);
101        assert_eq!(cfg.max_wasm_size, None);
102        assert_eq!(cfg.allow_call_schemes, None);
103    }
104
105    #[test]
106    fn deserialises_allow_call_schemes() {
107        let toml = toml::toml! {
108            timeout-secs = 10
109            allow-call-schemes = "log,direct"
110        };
111        let cfg: WasmLimitsConfig = toml.try_into().expect("deserialize");
112        assert_eq!(cfg.allow_call_schemes.as_deref(), Some("log,direct"));
113    }
114
115    #[test]
116    fn rejects_unknown_field() {
117        let toml = toml::toml! {
118            timeout-secs = 10
119            fuel = 1000
120        };
121        let result: Result<WasmLimitsConfig, _> = toml.try_into();
122        assert!(result.is_err(), "deny_unknown_fields must reject `fuel`");
123    }
124
125    #[test]
126    fn serde_round_trip_preserves_set_fields() {
127        let original = WasmLimitsConfig {
128            timeout_secs: Some(45),
129            max_memory: Some(64 * 1024 * 1024),
130            max_concurrent_calls: None,
131            ..WasmLimitsConfig::default()
132        };
133        let serialized = toml::to_string(&original).expect("serialize");
134        let back: WasmLimitsConfig = toml::from_str(&serialized).expect("deserialize");
135        assert_eq!(original, back);
136    }
137
138    #[test]
139    fn skip_serializing_none_fields() {
140        let cfg = WasmLimitsConfig {
141            timeout_secs: Some(30),
142            max_memory: None,
143            max_concurrent_calls: None,
144            ..WasmLimitsConfig::default()
145        };
146        let s = toml::to_string(&cfg).expect("serialize");
147        assert!(s.contains("timeout-secs"));
148        assert!(!s.contains("max-memory"));
149        assert!(!s.contains("max-concurrent-calls"));
150        assert!(!s.contains("max-wasm-size"));
151        assert!(!s.contains("allow-call-schemes"));
152    }
153}