use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::governance::EffectVector;
pub(crate) mod sig_serde {
use serde::{self, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(hash: &[u8; 64], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect();
serializer.serialize_str(&hex)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 64], D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let bytes: Vec<u8> = (0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(serde::de::Error::custom))
.collect::<Result<Vec<u8>, _>>()?;
let mut arr = [0u8; 64];
if bytes.len() != 64 {
return Err(serde::de::Error::custom(format!(
"expected 64 bytes, got {}",
bytes.len()
)));
}
arr.copy_from_slice(&bytes);
Ok(arr)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WasmSandboxConfig {
#[serde(default = "default_max_fuel")]
pub max_fuel: u64,
#[serde(default = "default_max_memory")]
pub max_memory_bytes: usize,
#[serde(default = "default_max_execution_secs", alias = "maxExecutionTimeSecs")]
pub max_execution_time_secs: u64,
#[serde(default)]
pub allowed_host_calls: Vec<String>,
#[serde(default)]
pub wasi_enabled: bool,
#[serde(default = "default_max_module_size")]
pub max_module_size_bytes: usize,
}
fn default_max_fuel() -> u64 {
1_000_000
}
fn default_max_memory() -> usize {
16 * 1024 * 1024 }
fn default_max_execution_secs() -> u64 {
30
}
fn default_max_module_size() -> usize {
10 * 1024 * 1024 }
impl Default for WasmSandboxConfig {
fn default() -> Self {
Self {
max_fuel: default_max_fuel(),
max_memory_bytes: default_max_memory(),
max_execution_time_secs: default_max_execution_secs(),
allowed_host_calls: Vec::new(),
wasi_enabled: false,
max_module_size_bytes: default_max_module_size(),
}
}
}
impl WasmSandboxConfig {
pub fn execution_timeout(&self) -> Duration {
Duration::from_secs(self.max_execution_time_secs)
}
}
#[derive(Debug, Clone, Default)]
pub struct ToolState {
pub tool_name: String,
pub stdin: Vec<u8>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WasmToolResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub fuel_consumed: u64,
pub memory_peak: usize,
#[serde(with = "duration_millis")]
pub execution_time: Duration,
}
mod duration_millis {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::Duration;
pub fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
d.as_millis().serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
let ms = u64::deserialize(d)?;
Ok(Duration::from_millis(ms))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WasmValidation {
pub valid: bool,
pub exports: Vec<String>,
pub imports: Vec<String>,
pub estimated_memory: usize,
pub warnings: Vec<String>,
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum WasmError {
#[error("WASM runtime unavailable: compile with --features wasm-sandbox")]
RuntimeUnavailable,
#[error("invalid WASM module: {0}")]
InvalidModule(String),
#[error("compilation failed: {0}")]
CompilationFailed(String),
#[error("fuel exhausted after {consumed} units (limit: {limit})")]
FuelExhausted {
consumed: u64,
limit: u64,
},
#[error("memory limit exceeded: {allocated} bytes (limit: {limit} bytes)")]
MemoryLimitExceeded {
allocated: usize,
limit: usize,
},
#[error("execution timeout after {0:?}")]
ExecutionTimeout(Duration),
#[error("WASM trap: {0}")]
WasmTrap(String),
#[error("host call denied: {0}")]
HostCallDenied(String),
#[error("module too large: {size} bytes (limit: {limit} bytes)")]
ModuleTooLarge {
size: usize,
limit: usize,
},
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum ToolError {
#[error("tool not found: {0}")]
NotFound(String),
#[error("invalid arguments: {0}")]
InvalidArgs(String),
#[error("execution failed: {0}")]
ExecutionFailed(String),
#[error("file not found: {0}")]
FileNotFound(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("file too large: {size} bytes (limit: {limit} bytes)")]
FileTooLarge { size: u64, limit: u64 },
#[error("signature required: {0}")]
SignatureRequired(String),
#[error("invalid signature: {0}")]
InvalidSignature(String),
#[error("wasm error: {0}")]
Wasm(#[from] WasmError),
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolCategory {
Filesystem,
Agent,
System,
Ecc,
User,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuiltinToolSpec {
pub name: String,
pub category: ToolCategory,
pub description: String,
pub parameters: serde_json::Value,
pub gate_action: String,
pub effect: EffectVector,
pub native: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolVersion {
pub version: u32,
pub module_hash: [u8; 32],
#[serde(with = "sig_serde")]
pub signature: [u8; 64],
pub deployed_at: DateTime<Utc>,
pub revoked: bool,
pub chain_seq: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeployedTool {
pub spec: BuiltinToolSpec,
pub versions: Vec<ToolVersion>,
pub active_version: u32,
}
#[derive(Debug, Clone)]
pub struct WasmTool {
pub name: String,
pub module_size: usize,
pub module_hash: [u8; 32],
pub schema: Option<serde_json::Value>,
pub exports: Vec<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum WasiFsScope {
#[default]
None,
ReadOnly(PathBuf),
ReadWrite(PathBuf),
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ToolSigningAuthority {
Kernel,
Developer {
cert_chain: Vec<Certificate>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Certificate {
pub subject: String,
pub public_key: [u8; 32],
#[serde(with = "sig_serde")]
pub signature: [u8; 64],
pub issuer: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSignature {
pub tool_name: String,
pub tool_hash: [u8; 32],
pub signer_id: String,
pub signature: Vec<u8>,
pub signed_at: DateTime<Utc>,
}
impl ToolSignature {
pub fn new(
tool_name: impl Into<String>,
tool_hash: [u8; 32],
signer_id: impl Into<String>,
signature: Vec<u8>,
) -> Self {
Self {
tool_name: tool_name.into(),
tool_hash,
signer_id: signer_id.into(),
signature,
signed_at: Utc::now(),
}
}
pub fn verify(&self, public_key: &[u8; 32]) -> bool {
if self.signature.len() != 64 {
return false;
}
let mut sig_bytes = [0u8; 64];
sig_bytes.copy_from_slice(&self.signature);
verify_tool_signature(&self.tool_hash, &sig_bytes, public_key)
}
}
#[cfg(feature = "exochain")]
pub fn verify_tool_signature(
module_hash: &[u8; 32],
signature: &[u8; 64],
public_key: &[u8; 32],
) -> bool {
use ed25519_dalek::{Verifier, VerifyingKey, Signature};
let Ok(vk) = VerifyingKey::from_bytes(public_key) else {
return false;
};
let sig = Signature::from_bytes(signature);
vk.verify(module_hash, &sig).is_ok()
}
#[cfg(not(feature = "exochain"))]
pub fn verify_tool_signature(
_module_hash: &[u8; 32],
_signature: &[u8; 64],
_public_key: &[u8; 32],
) -> bool {
false
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackendSelection {
Native,
Wasm,
Auto,
}
impl BackendSelection {
pub fn from_risk(risk: f64) -> Self {
if risk > 0.3 {
Self::Wasm
} else {
Self::Native
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SandboxLayer {
Governance,
Environment,
SudoOverride,
}
impl std::fmt::Display for SandboxLayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SandboxLayer::Governance => write!(f, "governance"),
SandboxLayer::Environment => write!(f, "environment"),
SandboxLayer::SudoOverride => write!(f, "sudo-override"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxDecision {
pub allowed: bool,
pub decided_by: SandboxLayer,
pub reason: String,
}
impl SandboxDecision {
pub fn permit(layer: SandboxLayer) -> Self {
Self {
allowed: true,
decided_by: layer,
reason: "access permitted".into(),
}
}
pub fn deny(layer: SandboxLayer, reason: impl Into<String>) -> Self {
Self {
allowed: false,
decided_by: layer,
reason: reason.into(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SandboxConfig {
pub allowed_paths: Vec<PathBuf>,
#[serde(default)]
pub sudo_override: bool,
}
impl SandboxConfig {
pub fn is_path_allowed(&self, path: &std::path::Path) -> bool {
if self.allowed_paths.is_empty() {
return true;
}
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
self.allowed_paths.iter().any(|allowed| {
let allowed_canon = allowed.canonicalize().unwrap_or_else(|_| allowed.clone());
canonical.starts_with(&allowed_canon)
})
}
pub fn check_path_multilayer(&self, path: &std::path::Path) -> SandboxDecision {
if self.allowed_paths.is_empty() {
return SandboxDecision::permit(SandboxLayer::Environment);
}
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let env_allowed = self.allowed_paths.iter().any(|allowed| {
let allowed_canon = allowed.canonicalize().unwrap_or_else(|_| allowed.clone());
canonical.starts_with(&allowed_canon)
});
if env_allowed {
return SandboxDecision::permit(SandboxLayer::Environment);
}
if self.sudo_override {
return SandboxDecision {
allowed: true,
decided_by: SandboxLayer::SudoOverride,
reason: format!(
"sudo override: path {} bypassed environment restriction",
path.display()
),
};
}
SandboxDecision::deny(
SandboxLayer::Environment,
format!("path outside sandbox: {}", path.display()),
)
}
}