Pumpkin/crates/pumpkin-config/src/plugins.rs
2026-08-16 22:28:01 +02:00

168 lines
6.4 KiB
Rust

use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// Plugin system configuration.
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct PluginsConfig {
/// Whether the plugin system is enabled. If false, no plugins will be loaded.
pub enabled: bool,
/// Whether to watch the plugins directory and automatically hot-reload modified plugins.
pub hot_reload: bool,
/// Whether the server asks for confirmation in the console when a plugin requests new permissions.
pub ask_permission_confirmation: bool,
/// Whether to allow loading unsigned WASM plugins.
pub allow_unsigned: bool,
/// List of permissions that are globally pre-approved for all plugins (bypassing confirmation).
pub allowed_permissions: Vec<String>,
/// List of permissions that are globally blocked for all plugins.
pub blocked_permissions: Vec<String>,
/// Whether host environment variables are inherited into WASI environments by default without explicit permissions.
pub inherit_env: bool,
/// Whether network sockets in plugins are restricted to localhost/loopback by default.
pub loopback_only: bool,
/// Optional global maximum memory limit in megabytes (MB) per plugin instance.
/// If not set, memory is only constrained by the host system's available memory.
pub max_memory_mb: Option<u64>,
/// Per-plugin configuration and overrides, keyed by plugin name.
///
/// Each entry is named after the plugin it applies to (for example `my_plugin`).
/// You only need to list the plugins you actually want to configure or override.
///
/// Example:
///
/// ```toml
/// [plugins.overrides.my_plugin]
/// enabled = true
/// allow_unsigned = true
/// max_memory_mb = 128
/// allowed_permissions = ["fs:read:data", "fs:write:data"]
/// blocked_permissions = ["network:outbound"]
/// loopback_only = true
///
/// [plugins.overrides.my_plugin.environment]
/// MY_API_KEY = "secret_key"
/// ```
pub overrides: HashMap<String, PluginOverride>,
}
impl Default for PluginsConfig {
fn default() -> Self {
Self {
enabled: true,
hot_reload: false,
ask_permission_confirmation: true,
allow_unsigned: true,
allowed_permissions: Vec::new(),
blocked_permissions: Vec::new(),
inherit_env: false,
loopback_only: false,
max_memory_mb: None,
overrides: HashMap::new(),
}
}
}
/// Settings for a single plugin, letting a server owner turn it off or change
/// its permissions, unsigned execution policy, memory limit, or environment variables.
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct PluginOverride {
/// Whether this specific plugin is enabled. If set to `false`, the plugin will be ignored during loading.
pub enabled: bool,
/// Override whether this plugin is allowed to run if unsigned.
pub allow_unsigned: Option<bool>,
/// Optional maximum memory limit in megabytes (MB) for this specific plugin.
/// Overrides the global `max_memory_mb` setting if specified.
pub max_memory_mb: Option<u64>,
/// Permissions pre-approved specifically for this plugin (skips interactive confirmation).
pub allowed_permissions: Vec<String>,
/// Additional permissions blocked specifically for this plugin.
pub blocked_permissions: Vec<String>,
/// Override whether network access is restricted to loopback for this plugin.
pub loopback_only: Option<bool>,
/// Custom environment variables passed directly to this plugin's WASI environment.
pub environment: HashMap<String, String>,
}
impl Default for PluginOverride {
fn default() -> Self {
Self {
enabled: true,
allow_unsigned: None,
max_memory_mb: None,
allowed_permissions: Vec::new(),
blocked_permissions: Vec::new(),
loopback_only: None,
environment: HashMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let config = PluginsConfig::default();
assert!(config.enabled);
assert!(!config.hot_reload);
assert!(config.ask_permission_confirmation);
assert!(config.allow_unsigned);
assert!(config.allowed_permissions.is_empty());
assert!(config.blocked_permissions.is_empty());
assert!(!config.inherit_env);
assert!(!config.loopback_only);
assert_eq!(config.max_memory_mb, None);
assert!(config.overrides.is_empty());
}
#[test]
fn parse_toml_overrides() {
let toml_str = r#"
enabled = true
hot_reload = true
ask_permission_confirmation = false
allow_unsigned = false
allowed_permissions = ["fs:read:data"]
blocked_permissions = ["network:outbound"]
inherit_env = true
loopback_only = true
max_memory_mb = 256
[overrides.my_plugin]
enabled = false
allow_unsigned = true
max_memory_mb = 128
allowed_permissions = ["network:tcp"]
blocked_permissions = ["fs:write:data"]
loopback_only = false
[overrides.my_plugin.environment]
API_KEY = "12345"
"#;
let config: PluginsConfig = toml::from_str(toml_str).unwrap();
assert!(config.enabled);
assert!(config.hot_reload);
assert!(!config.ask_permission_confirmation);
assert!(!config.allow_unsigned);
assert_eq!(config.allowed_permissions, vec!["fs:read:data"]);
assert_eq!(config.blocked_permissions, vec!["network:outbound"]);
assert!(config.inherit_env);
assert!(config.loopback_only);
assert_eq!(config.max_memory_mb, Some(256));
let override_cfg = config.overrides.get("my_plugin").unwrap();
assert!(!override_cfg.enabled);
assert_eq!(override_cfg.allow_unsigned, Some(true));
assert_eq!(override_cfg.max_memory_mb, Some(128));
assert_eq!(override_cfg.allowed_permissions, vec!["network:tcp"]);
assert_eq!(override_cfg.blocked_permissions, vec!["fs:write:data"]);
assert_eq!(override_cfg.loopback_only, Some(false));
assert_eq!(override_cfg.environment.get("API_KEY").unwrap(), "12345");
}
}