use crate::plugin::{PluginRuntimeLimitError, PluginRuntimeLimitField};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::num::{NonZeroU64, NonZeroUsize};
pub const MAX_PLUGIN_MEMORY_BYTES: u64 = 256 * 1024 * 1024;
pub const MAX_PLUGIN_MESSAGE_BYTES: u64 = 8 * 1024 * 1024;
pub const MAX_PLUGIN_FUEL_PER_UPDATE: u64 = 100_000_000;
pub const MAX_PLUGIN_TIMEOUT_MS: u64 = 1_000;
pub const MAX_PLUGIN_INTENT_BATCHES: u64 = 1_024;
#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct PluginRuntimeLimits {
pub max_memory_bytes: u64,
pub max_message_bytes: u64,
pub fuel_per_update: u64,
pub timeout_ms: u64,
pub max_intent_batches: u64,
}
impl PluginRuntimeLimits {
pub fn try_new(
max_memory_bytes: u64,
max_message_bytes: u64,
fuel_per_update: u64,
timeout_ms: u64,
) -> Result<Self, PluginRuntimeLimitError> {
let limits = Self {
max_memory_bytes,
max_message_bytes,
fuel_per_update,
timeout_ms,
max_intent_batches: Self::default().max_intent_batches,
};
let _validated = limits.validate()?;
Ok(limits)
}
pub fn validate(self) -> Result<ValidatedPluginRuntimeLimits, PluginRuntimeLimitError> {
ValidatedPluginRuntimeLimits::try_from_config(self)
}
}
impl Default for PluginRuntimeLimits {
fn default() -> Self {
Self {
max_memory_bytes: 16 * 1024 * 1024,
max_message_bytes: 1024 * 1024,
fuel_per_update: 1_000_000,
timeout_ms: 50,
max_intent_batches: MAX_PLUGIN_INTENT_BATCHES,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ValidatedPluginRuntimeLimits {
max_memory_bytes: NonZeroU64,
max_message_bytes: NonZeroU64,
fuel_per_update: NonZeroU64,
timeout_ms: NonZeroU64,
max_intent_batches: NonZeroUsize,
}
impl ValidatedPluginRuntimeLimits {
pub fn try_from_config(limits: PluginRuntimeLimits) -> Result<Self, PluginRuntimeLimitError> {
let max_memory_bytes = validate_limit(
PluginRuntimeLimitField::MaxMemoryBytes,
limits.max_memory_bytes,
MAX_PLUGIN_MEMORY_BYTES,
)?;
let max_message_bytes = validate_limit(
PluginRuntimeLimitField::MaxMessageBytes,
limits.max_message_bytes,
MAX_PLUGIN_MESSAGE_BYTES,
)?;
let fuel_per_update = validate_limit(
PluginRuntimeLimitField::FuelPerUpdate,
limits.fuel_per_update,
MAX_PLUGIN_FUEL_PER_UPDATE,
)?;
let timeout_ms = validate_limit(
PluginRuntimeLimitField::TimeoutMs,
limits.timeout_ms,
MAX_PLUGIN_TIMEOUT_MS,
)?;
let max_intent_batches = validate_host_count_limit(
PluginRuntimeLimitField::MaxIntentBatches,
limits.max_intent_batches,
MAX_PLUGIN_INTENT_BATCHES,
)?;
Ok(Self {
max_memory_bytes,
max_message_bytes,
fuel_per_update,
timeout_ms,
max_intent_batches,
})
}
#[must_use]
pub const fn as_config(self) -> PluginRuntimeLimits {
PluginRuntimeLimits {
max_memory_bytes: self.max_memory_bytes.get(),
max_message_bytes: self.max_message_bytes.get(),
fuel_per_update: self.fuel_per_update.get(),
timeout_ms: self.timeout_ms.get(),
max_intent_batches: self.max_intent_batches.get() as u64,
}
}
#[must_use]
pub const fn max_memory_bytes(self) -> u64 {
self.max_memory_bytes.get()
}
#[must_use]
pub const fn max_message_bytes(self) -> u64 {
self.max_message_bytes.get()
}
#[must_use]
pub(in crate::plugin) const fn max_message_byte_limit(self) -> NonZeroU64 {
self.max_message_bytes
}
#[must_use]
pub const fn fuel_per_update(self) -> u64 {
self.fuel_per_update.get()
}
#[must_use]
pub const fn timeout_ms(self) -> u64 {
self.timeout_ms.get()
}
#[must_use]
pub const fn max_intent_batches(self) -> usize {
self.max_intent_batches.get()
}
}
impl Default for ValidatedPluginRuntimeLimits {
fn default() -> Self {
PluginRuntimeLimits::default()
.validate()
.expect("default plugin runtime limits should validate")
}
}
const fn validate_limit(
field: PluginRuntimeLimitField,
value: u64,
max: u64,
) -> Result<NonZeroU64, PluginRuntimeLimitError> {
let Some(value) = NonZeroU64::new(value) else {
return Err(PluginRuntimeLimitError::Zero { field });
};
if value.get() > max {
return Err(PluginRuntimeLimitError::TooLarge {
field,
value: value.get(),
max,
});
}
Ok(value)
}
fn validate_host_count_limit(
field: PluginRuntimeLimitField,
value: u64,
max: u64,
) -> Result<NonZeroUsize, PluginRuntimeLimitError> {
let value = validate_limit(field, value, max)?;
let value =
usize::try_from(value.get()).expect("validated plugin host count limit should fit usize");
Ok(NonZeroUsize::new(value).expect("validated plugin host count limit should be non-zero"))
}
#[cfg(test)]
mod tests {
use super::{
MAX_PLUGIN_FUEL_PER_UPDATE, MAX_PLUGIN_INTENT_BATCHES, MAX_PLUGIN_MEMORY_BYTES,
MAX_PLUGIN_MESSAGE_BYTES, MAX_PLUGIN_TIMEOUT_MS, PluginRuntimeLimitField,
PluginRuntimeLimits,
};
use crate::plugin::PluginRuntimeLimitError;
use proptest::prelude::*;
proptest! {
#[test]
fn runtime_limits_reject_zero_values(field in runtime_limit_field_strategy()) {
prop_assert_eq!(
field.with_value(0).validate(),
Err(PluginRuntimeLimitError::Zero {
field,
})
);
}
#[test]
fn runtime_limits_reject_values_above_host_caps(
field in runtime_limit_field_strategy(),
excess in 1_u64..=1024,
) {
let value = field.max().saturating_add(excess);
prop_assert_eq!(
field.with_value(value).validate(),
Err(PluginRuntimeLimitError::TooLarge {
field,
value,
max: field.max(),
})
);
}
#[test]
fn runtime_limits_accept_nonzero_values_under_host_caps(
max_memory_bytes in 1_u64..=MAX_PLUGIN_MEMORY_BYTES,
max_message_bytes in 1_u64..=MAX_PLUGIN_MESSAGE_BYTES,
fuel_per_update in 1_u64..=MAX_PLUGIN_FUEL_PER_UPDATE,
timeout_ms in 1_u64..=MAX_PLUGIN_TIMEOUT_MS,
max_intent_batches in 1_u64..=MAX_PLUGIN_INTENT_BATCHES,
) {
let limits = PluginRuntimeLimits {
max_memory_bytes,
max_message_bytes,
fuel_per_update,
timeout_ms,
max_intent_batches,
};
let validated = limits.validate().expect("generated limits should validate");
prop_assert_eq!(validated.as_config(), limits);
prop_assert_eq!(validated.max_memory_bytes(), max_memory_bytes);
prop_assert_eq!(validated.max_message_bytes(), max_message_bytes);
prop_assert_eq!(validated.fuel_per_update(), fuel_per_update);
prop_assert_eq!(validated.timeout_ms(), timeout_ms);
prop_assert_eq!(
validated.max_intent_batches(),
usize::try_from(max_intent_batches)
.expect("validated intent batch limit should fit usize")
);
}
#[test]
fn runtime_limits_try_new_preserves_public_constructor_invariants(
max_memory_bytes in 1_u64..=MAX_PLUGIN_MEMORY_BYTES,
max_message_bytes in 1_u64..=MAX_PLUGIN_MESSAGE_BYTES,
fuel_per_update in 1_u64..=MAX_PLUGIN_FUEL_PER_UPDATE,
timeout_ms in 1_u64..=MAX_PLUGIN_TIMEOUT_MS,
) {
let limits = PluginRuntimeLimits::try_new(
max_memory_bytes,
max_message_bytes,
fuel_per_update,
timeout_ms,
)
.expect("generated limits should validate");
prop_assert_eq!(limits.max_memory_bytes, max_memory_bytes);
prop_assert_eq!(limits.max_message_bytes, max_message_bytes);
prop_assert_eq!(limits.fuel_per_update, fuel_per_update);
prop_assert_eq!(limits.timeout_ms, timeout_ms);
prop_assert_eq!(
limits.max_intent_batches,
PluginRuntimeLimits::default().max_intent_batches
);
prop_assert_eq!(limits.validate().expect("limits should revalidate").as_config(), limits);
}
}
#[test]
fn runtime_limit_fields_render_stable_text() {
assert_eq!(
PluginRuntimeLimitField::MaxMemoryBytes.as_str(),
"max_memory_bytes"
);
assert_eq!(
PluginRuntimeLimitField::MaxMessageBytes.to_string(),
"max_message_bytes"
);
assert_eq!(
PluginRuntimeLimitField::FuelPerUpdate.to_string(),
"fuel_per_update"
);
assert_eq!(PluginRuntimeLimitField::TimeoutMs.to_string(), "timeout_ms");
assert_eq!(
PluginRuntimeLimitField::MaxIntentBatches.to_string(),
"max_intent_batches"
);
}
impl PluginRuntimeLimitField {
const fn max(self) -> u64 {
match self {
Self::MaxMemoryBytes => MAX_PLUGIN_MEMORY_BYTES,
Self::MaxMessageBytes => MAX_PLUGIN_MESSAGE_BYTES,
Self::FuelPerUpdate => MAX_PLUGIN_FUEL_PER_UPDATE,
Self::TimeoutMs => MAX_PLUGIN_TIMEOUT_MS,
Self::MaxIntentBatches => MAX_PLUGIN_INTENT_BATCHES,
}
}
fn with_value(self, value: u64) -> PluginRuntimeLimits {
match self {
Self::MaxMemoryBytes => PluginRuntimeLimits {
max_memory_bytes: value,
..PluginRuntimeLimits::default()
},
Self::MaxMessageBytes => PluginRuntimeLimits {
max_message_bytes: value,
..PluginRuntimeLimits::default()
},
Self::FuelPerUpdate => PluginRuntimeLimits {
fuel_per_update: value,
..PluginRuntimeLimits::default()
},
Self::TimeoutMs => PluginRuntimeLimits {
timeout_ms: value,
..PluginRuntimeLimits::default()
},
Self::MaxIntentBatches => PluginRuntimeLimits {
max_intent_batches: value,
..PluginRuntimeLimits::default()
},
}
}
}
fn runtime_limit_field_strategy() -> impl Strategy<Value = PluginRuntimeLimitField> {
prop_oneof![
Just(PluginRuntimeLimitField::MaxMemoryBytes),
Just(PluginRuntimeLimitField::MaxMessageBytes),
Just(PluginRuntimeLimitField::FuelPerUpdate),
Just(PluginRuntimeLimitField::TimeoutMs),
Just(PluginRuntimeLimitField::MaxIntentBatches),
]
}
}