use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::path::PathBuf;
use tracing::debug;
use super::inheritance::{is_default_permission_policy, HandlerPermissionPolicy};
use super::permissions::HandlerPermission;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageConfig {
pub name: String,
pub package_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestConfig {
pub name: String,
pub version: String,
pub package: String,
pub description: Option<String>,
pub long_description: Option<String>,
pub initial_state: Option<String>,
pub save_chain: Option<bool>,
#[serde(default, skip_serializing_if = "is_default_permission_policy")]
pub permission_policy: HandlerPermissionPolicy,
#[serde(default, rename = "handler")]
pub handlers: Vec<HandlerConfig>,
}
impl Display for ManifestConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ManifestConfig(name: {}, version: {}, package: {}, description: {:?}, long_description: {:?}, initial_state: {:?}, save_chain: {:?}, permission_policy: {:?}, handlers: {:?})",
self.name,
self.version,
self.package,
self.description,
self.long_description,
self.initial_state,
self.save_chain,
self.permission_policy,
self.handlers
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventServerConfig {
pub port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct LoggingConfig {
pub chain_events: bool,
pub level: String,
pub output: LogOutput,
pub log_dir: Option<PathBuf>,
pub file_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogOutput {
Stdout,
File,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
chain_events: true,
level: "info".to_string(),
output: LogOutput::File,
log_dir: Some(PathBuf::from("logs")),
file_path: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct InterfacesConfig {
#[serde(default)]
pub implements: String,
pub requires: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum HandlerConfig {
#[serde(rename = "message-server")]
MessageServer {
#[serde(flatten)]
config: MessageServerConfig,
},
#[serde(rename = "filesystem")]
FileSystem {
#[serde(flatten)]
config: FileSystemHandlerConfig,
},
#[serde(rename = "http-client")]
HttpClient {
#[serde(flatten)]
config: HttpClientHandlerConfig,
},
#[serde(rename = "http-framework")]
HttpFramework {
#[serde(flatten)]
config: HttpFrameworkHandlerConfig,
},
#[serde(rename = "runtime")]
Runtime {
#[serde(flatten)]
config: RuntimeHostConfig,
},
#[serde(rename = "supervisor")]
Supervisor {
#[serde(flatten)]
config: SupervisorHostConfig,
},
#[serde(rename = "store")]
Store {
#[serde(flatten)]
config: StoreHandlerConfig,
},
#[serde(rename = "timing")]
Timing {
#[serde(flatten)]
config: TimingHostConfig,
},
#[serde(rename = "process")]
Process {
#[serde(flatten)]
config: ProcessHostConfig,
},
#[serde(rename = "environment")]
Environment {
#[serde(flatten)]
config: EnvironmentHandlerConfig,
},
#[serde(rename = "random")]
Random {
#[serde(flatten)]
config: RandomHandlerConfig,
},
#[serde(rename = "wasi-http")]
WasiHttp {
#[serde(flatten)]
config: WasiHttpHandlerConfig,
},
#[serde(rename = "replay")]
Replay {
#[serde(flatten)]
config: ReplayHandlerConfig,
},
#[serde(rename = "tcp")]
Tcp {
#[serde(flatten)]
config: TcpHandlerConfig,
},
#[serde(rename = "rpc")]
Rpc {
#[serde(flatten)]
config: RpcHandlerConfig,
},
#[serde(rename = "terminal")]
Terminal {
#[serde(flatten)]
config: TerminalHandlerConfig,
},
#[serde(rename = "timer")]
Timer {
#[serde(flatten)]
config: TimerHandlerConfig,
},
#[serde(rename = "loop")]
Loop {
#[serde(flatten)]
config: LoopHandlerConfig,
},
}
impl HandlerConfig {
pub fn handler_name(&self) -> &'static str {
match self {
HandlerConfig::MessageServer { .. } => "message-server",
HandlerConfig::FileSystem { .. } => "filesystem",
HandlerConfig::HttpClient { .. } => "http-client",
HandlerConfig::HttpFramework { .. } => "http-framework",
HandlerConfig::Runtime { .. } => "runtime",
HandlerConfig::Supervisor { .. } => "supervisor",
HandlerConfig::Store { .. } => "store",
HandlerConfig::Timing { .. } => "timing",
HandlerConfig::Process { .. } => "process",
HandlerConfig::Environment { .. } => "environment",
HandlerConfig::Random { .. } => "random",
HandlerConfig::WasiHttp { .. } => "wasi-http",
HandlerConfig::Replay { .. } => "replay",
HandlerConfig::Tcp { .. } => "tcp",
HandlerConfig::Rpc { .. } => "rpc",
HandlerConfig::Terminal { .. } => "terminal",
HandlerConfig::Timer { .. } => "timer",
HandlerConfig::Loop { .. } => "loop",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ReplayHandlerConfig {
pub chain: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct TcpHandlerConfig {
#[serde(default)]
pub listen: Option<String>,
#[serde(default)]
pub max_connections: Option<u32>,
#[serde(default)]
pub client_tls: Option<ClientTlsConfig>,
#[serde(default)]
pub server_tls: Option<ServerTlsConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ClientTlsConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub ca_cert: Option<PathBuf>,
#[serde(default)]
pub skip_verify: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ServerTlsConfig {
#[serde(default)]
pub enabled: bool,
pub cert: PathBuf,
pub key: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct RpcHandlerConfig {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct TerminalHandlerConfig {
#[serde(default)]
pub raw_mode: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct TimerHandlerConfig {
#[serde(default)]
pub interval_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct LoopHandlerConfig {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SupervisorHostConfig {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RuntimeHostConfig {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TimingHostConfig {
#[serde(default = "default_max_sleep_duration")]
pub max_sleep_duration: u64,
#[serde(default = "default_min_sleep_duration")]
pub min_sleep_duration: u64,
}
fn default_max_sleep_duration() -> u64 {
3600000
}
fn default_min_sleep_duration() -> u64 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpServerHandlerConfig {
pub port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebSocketServerHandlerConfig {
pub port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MessageServerConfig {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FileSystemHandlerConfig {
pub path: Option<PathBuf>,
pub new_dir: Option<bool>,
pub allowed_commands: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HttpClientHandlerConfig {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WasiHttpHandlerConfig {
#[serde(default)]
pub port: Option<u16>,
#[serde(default = "default_wasi_http_host")]
pub host: String,
}
fn default_wasi_http_host() -> String {
"127.0.0.1".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct StoreHandlerConfig {
#[serde(default)]
pub base_path: Option<std::path::PathBuf>,
#[serde(default)]
pub store_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HttpFrameworkHandlerConfig {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProcessHostConfig {
#[serde(default = "default_max_processes")]
pub max_processes: usize,
#[serde(default = "default_max_output_buffer")]
pub max_output_buffer: usize,
pub allowed_programs: Option<Vec<String>>,
pub allowed_paths: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EnvironmentHandlerConfig {
pub allowed_vars: Option<Vec<String>>,
pub denied_vars: Option<Vec<String>>,
#[serde(default)]
pub allow_list_all: bool,
pub allowed_prefixes: Option<Vec<String>>,
}
impl EnvironmentHandlerConfig {
pub fn is_variable_allowed(&self, var_name: &str) -> bool {
if let Some(denied) = &self.denied_vars {
if denied.contains(&var_name.to_string()) {
return false;
}
}
if let Some(allowed) = &self.allowed_vars {
return allowed.contains(&var_name.to_string());
}
if let Some(prefixes) = &self.allowed_prefixes {
return prefixes.iter().any(|prefix| var_name.starts_with(prefix));
}
self.denied_vars.is_none()
|| !self
.denied_vars
.as_ref()
.unwrap()
.contains(&var_name.to_string())
}
}
fn default_max_processes() -> usize {
10
}
fn default_max_output_buffer() -> usize {
1024 * 1024
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RandomHandlerConfig {
pub seed: Option<u64>,
#[serde(default = "default_max_random_bytes")]
pub max_bytes: usize,
#[serde(default = "default_max_random_int")]
pub max_int: u64,
#[serde(default)]
pub allow_crypto_secure: bool,
}
fn default_max_random_bytes() -> usize {
1024 * 1024 }
fn default_max_random_int() -> u64 {
9223372036854775807 }
impl ManifestConfig {
pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> anyhow::Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: ManifestConfig = toml::from_str(&content)?;
Ok(config)
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(content: &str) -> anyhow::Result<Self> {
tracing::info!("Parsing manifest TOML content: {}", content);
let config: ManifestConfig = match toml::from_str(content) {
Ok(config) => {
tracing::info!("Successfully parsed manifest TOML");
config
}
Err(e) => {
tracing::error!("Failed to parse manifest TOML: {}", e);
return Err(e.into());
}
};
tracing::info!(
"Parsed manifest permission_policy: {:?}",
config.permission_policy
);
tracing::info!(
"Parsed manifest file_system inheritance: {:?}",
config.permission_policy.file_system
);
Ok(config)
}
pub fn from_vec(content: Vec<u8>) -> anyhow::Result<Self> {
let config: ManifestConfig = toml::from_str(&String::from_utf8(content)?)?;
Ok(config)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn from_toml_str(content: &str) -> anyhow::Result<Self> {
Self::from_str(content)
}
pub fn into_fixed_bytes(self) -> Result<Vec<u8>, anyhow::Error> {
debug!("Serializing manifest config to fixed bytes");
debug!("Manifest config: {:?}", self);
let serialized = toml::to_string(&self)
.map_err(|e| anyhow::anyhow!("Failed to serialize manifest: {}", e))?;
debug!("Serialized manifest config: {}", serialized);
Ok(serialized.into_bytes())
}
pub fn save_chain(&self) -> bool {
self.save_chain.unwrap_or(false)
}
pub fn calculate_effective_permissions(
&self,
parent_permissions: &HandlerPermission,
) -> HandlerPermission {
tracing::info!(
"Calculating effective permissions from parent: {:?}",
parent_permissions
);
tracing::info!("Using permission policy: {:?}", self.permission_policy);
let effective =
HandlerPermission::calculate_effective(parent_permissions, &self.permission_policy);
tracing::info!("Calculated effective permissions: {:?}", effective);
tracing::info!(
"Effective filesystem permissions: {:?}",
effective.file_system
);
effective
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_permission_policy_parsing() {
let toml_content = r#"
name = "test-actor"
version = "0.1.0"
package = "test.wasm"
[permission_policy.file_system]
type = "restrict"
[permission_policy.file_system.config]
read = true
write = true
execute = false
allowed_paths = ["/tmp/test"]
"#;
let manifest = ManifestConfig::from_str(toml_content).unwrap();
match &manifest.permission_policy.file_system {
crate::config::inheritance::HandlerInheritance::Restrict(perms) => {
assert!(perms.read);
assert!(perms.write);
assert!(!perms.execute);
assert_eq!(perms.allowed_paths, Some(vec!["/tmp/test".to_string()]));
}
_ => panic!("Expected Restrict variant with FileSystemPermissions"),
}
}
#[test]
fn test_manifest_wrong_permissions_structure() {
let toml_content = r#"
name = "test-actor"
version = "0.1.0"
package = "test.wasm"
[permissions.file_system]
read = true
write = true
execute = false
allowed_paths = ["/tmp/test"]
"#;
let manifest = ManifestConfig::from_str(toml_content).unwrap();
match &manifest.permission_policy.file_system {
crate::config::inheritance::HandlerInheritance::Inherit => {
println!("As expected, wrong structure results in Inherit");
}
other => panic!("Expected Inherit, got: {:?}", other),
}
}
}