use std::{
collections::{BTreeMap, BTreeSet},
fmt,
};
pub const WESLEY_CAPABILITY_ABI: &str = "wesley-capability-abi";
pub const CURRENT_CAPABILITY_ABI_VERSION: CapabilityContractVersion =
CapabilityContractVersion::new(0, 1, 0);
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityContractVersion {
pub major: u64,
pub minor: u64,
pub patch: u64,
}
impl CapabilityContractVersion {
pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
Self {
major,
minor,
patch,
}
}
}
impl fmt::Display for CapabilityContractVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityVersionRequirement {
pub abi: String,
pub minimum: CapabilityContractVersion,
pub maximum_exclusive: CapabilityContractVersion,
}
impl CapabilityVersionRequirement {
pub fn new(
abi: impl Into<String>,
minimum: CapabilityContractVersion,
maximum_exclusive: CapabilityContractVersion,
) -> Self {
Self {
abi: abi.into(),
minimum,
maximum_exclusive,
}
}
pub fn current() -> Self {
Self::new(
WESLEY_CAPABILITY_ABI,
CURRENT_CAPABILITY_ABI_VERSION,
CapabilityContractVersion::new(0, 2, 0),
)
}
pub fn allows(&self, abi: &str, version: CapabilityContractVersion) -> bool {
self.abi == abi && self.minimum <= version && version < self.maximum_exclusive
}
}
impl fmt::Display for CapabilityVersionRequirement {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{} >={} <{}",
self.abi, self.minimum, self.maximum_exclusive
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityExecutionMode {
RustNative,
Wasm,
ExternalProcess,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityPortabilityFloor {
HostNative,
PortableWasm,
ExternalProcess,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityRuntimeModel {
#[default]
Stateless,
ResourceHandles,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModuleTargetDescriptor {
pub module: String,
pub target: String,
pub is_default: bool,
pub execution_mode: CapabilityExecutionMode,
pub portability_floor: CapabilityPortabilityFloor,
#[serde(default = "CapabilityVersionRequirement::current")]
pub required_contract: CapabilityVersionRequirement,
#[serde(default)]
pub runtime_model: CapabilityRuntimeModel,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requested_host_imports: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requested_resource_handles: Vec<String>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ModuleTargetRegistry {
targets: BTreeMap<String, ModuleTargetDescriptor>,
}
impl ModuleTargetRegistry {
pub fn from_targets(
targets: impl IntoIterator<Item = ModuleTargetDescriptor>,
) -> Result<Self, ModuleCapabilityError> {
let mut registry = Self::default();
for target in targets {
registry.register(target)?;
}
Ok(registry)
}
pub fn register(
&mut self,
target: ModuleTargetDescriptor,
) -> Result<(), ModuleCapabilityError> {
if let Some(existing) = self.targets.get(&target.target) {
return Err(ModuleCapabilityError::DuplicateTarget {
target: target.target,
first_module: existing.module.clone(),
second_module: target.module,
});
}
self.targets.insert(target.target.clone(), target);
Ok(())
}
pub fn resolve_target(
&self,
requested: Option<&str>,
) -> Result<&ModuleTargetDescriptor, ModuleCapabilityError> {
if self.targets.is_empty() {
return Err(ModuleCapabilityError::NoTargets);
}
if let Some(requested) = requested {
return self.targets.get(requested).ok_or_else(|| {
ModuleCapabilityError::UnknownTarget {
target: requested.to_string(),
available: self.targets.keys().cloned().collect(),
}
});
}
let defaults = self
.targets
.values()
.filter(|target| target.is_default)
.collect::<Vec<_>>();
match defaults.as_slice() {
[target] => Ok(target),
[] => Err(ModuleCapabilityError::NoDefaultTarget {
available: self.targets.keys().cloned().collect(),
}),
targets => Err(ModuleCapabilityError::MultipleDefaultTargets {
targets: targets.iter().map(|target| target.target.clone()).collect(),
}),
}
}
pub fn capability_report(
&self,
requested: impl IntoIterator<Item = impl AsRef<str>>,
) -> CapabilityReport {
let mut requested_targets = Vec::new();
let mut granted = Vec::new();
let mut denied = Vec::new();
for target in requested {
let target = target.as_ref().to_string();
requested_targets.push(target.clone());
if self.targets.contains_key(&target) {
granted.push(target);
} else {
denied.push(target);
}
}
CapabilityReport {
requested: requested_targets,
granted,
denied,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityReport {
pub requested: Vec<String>,
pub granted: Vec<String>,
pub denied: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HostCapabilityContract {
pub host: String,
pub abi: String,
pub version: CapabilityContractVersion,
}
impl HostCapabilityContract {
pub fn current() -> Self {
Self {
host: "wesley-rust-host".to_string(),
abi: WESLEY_CAPABILITY_ABI.to_string(),
version: CURRENT_CAPABILITY_ABI_VERSION,
}
}
pub fn evaluate_contract(&self, target: &ModuleTargetDescriptor) -> CapabilityContractReport {
let mut diagnostics = Vec::new();
if target.required_contract.abi != self.abi {
diagnostics.push(CapabilityContractDiagnostic {
code: "MODULE_CONTRACT_ABI_MISMATCH".to_string(),
target: target.target.clone(),
host: self.host.clone(),
host_version: self.version.to_string(),
required: target.required_contract.to_string(),
});
} else if !target.required_contract.allows(&self.abi, self.version) {
diagnostics.push(CapabilityContractDiagnostic {
code: "WASM_ABI_UNSUPPORTED".to_string(),
target: target.target.clone(),
host: self.host.clone(),
host_version: self.version.to_string(),
required: target.required_contract.to_string(),
});
}
CapabilityContractReport {
target: target.target.clone(),
host: self.host.clone(),
host_version: self.version.to_string(),
required: target.required_contract.to_string(),
accepted: diagnostics.is_empty(),
diagnostics,
}
}
pub fn reject_incompatible_contract_before_execution(
&self,
target: &ModuleTargetDescriptor,
) -> Result<CapabilityContractReport, ModuleCapabilityError> {
let report = self.evaluate_contract(target);
if report.accepted {
Ok(report)
} else {
Err(ModuleCapabilityError::IncompatibleCapabilityContract {
target: target.target.clone(),
diagnostic_codes: report
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.clone())
.collect(),
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityContractReport {
pub target: String,
pub host: String,
pub host_version: String,
pub required: String,
pub accepted: bool,
pub diagnostics: Vec<CapabilityContractDiagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityContractDiagnostic {
pub code: String,
pub target: String,
pub host: String,
pub host_version: String,
pub required: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostFunctionPolicy {
profile: String,
allowed_imports: BTreeSet<String>,
}
impl HostFunctionPolicy {
pub fn pure() -> Self {
Self {
profile: "pure".to_string(),
allowed_imports: BTreeSet::new(),
}
}
pub fn allowing(
profile: impl Into<String>,
imports: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
profile: profile.into(),
allowed_imports: imports.into_iter().map(Into::into).collect(),
}
}
pub fn evaluate(&self, target: &ModuleTargetDescriptor) -> HostImportReport {
let requested = sorted_unique(&target.requested_host_imports);
let mut granted = Vec::new();
let mut denied = Vec::new();
for requested_import in &requested {
if self.allowed_imports.contains(requested_import) {
granted.push(requested_import.clone());
} else {
denied.push(requested_import.clone());
}
}
HostImportReport {
profile: self.profile.clone(),
target: target.target.clone(),
requested,
granted,
denied,
}
}
pub fn reject_unavailable_imports_before_execution(
&self,
target: &ModuleTargetDescriptor,
) -> Result<HostImportReport, ModuleCapabilityError> {
let report = self.evaluate(target);
if report.denied.is_empty() {
Ok(report)
} else {
Err(ModuleCapabilityError::DeniedHostImports {
target: target.target.clone(),
denied: report.denied,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HostImportReport {
pub profile: String,
pub target: String,
pub requested: Vec<String>,
pub granted: Vec<String>,
pub denied: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeResourcePolicy {
model: CapabilityRuntimeModel,
allowed_resource_handles: BTreeSet<String>,
}
impl RuntimeResourcePolicy {
pub fn stateless_default() -> Self {
Self {
model: CapabilityRuntimeModel::Stateless,
allowed_resource_handles: BTreeSet::new(),
}
}
pub fn allowing_resource_handles(handles: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
model: CapabilityRuntimeModel::ResourceHandles,
allowed_resource_handles: handles.into_iter().map(Into::into).collect(),
}
}
pub fn evaluate(&self, target: &ModuleTargetDescriptor) -> RuntimeResourceReport {
let requested = sorted_unique(&target.requested_resource_handles);
let mut granted = Vec::new();
let mut denied = Vec::new();
for requested_handle in &requested {
if self.model == CapabilityRuntimeModel::ResourceHandles
&& self.allowed_resource_handles.contains(requested_handle)
{
granted.push(requested_handle.clone());
} else {
denied.push(requested_handle.clone());
}
}
RuntimeResourceReport {
model: self.model,
target: target.target.clone(),
requested,
granted,
denied,
}
}
pub fn reject_resource_handles_before_execution(
&self,
target: &ModuleTargetDescriptor,
) -> Result<RuntimeResourceReport, ModuleCapabilityError> {
let report = self.evaluate(target);
if report.denied.is_empty() {
Ok(report)
} else {
Err(ModuleCapabilityError::DeniedResourceHandles {
target: target.target.clone(),
denied: report.denied,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeResourceReport {
pub model: CapabilityRuntimeModel,
pub target: String,
pub requested: Vec<String>,
pub granted: Vec<String>,
pub denied: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HermeticCapabilityFixture {
pub host: String,
pub target: String,
pub input_digest: String,
pub output_digest: String,
}
impl HermeticCapabilityFixture {
pub fn new(
host: impl Into<String>,
target: impl Into<String>,
input_digest: impl Into<String>,
output_digest: impl Into<String>,
) -> Self {
Self {
host: host.into(),
target: target.into(),
input_digest: input_digest.into(),
output_digest: output_digest.into(),
}
}
pub fn verify_cross_host_outputs(
fixtures: impl IntoIterator<Item = HermeticCapabilityFixture>,
) -> Result<HermeticCapabilityReport, ModuleCapabilityError> {
let fixtures = fixtures.into_iter().collect::<Vec<_>>();
let Some(first) = fixtures.first() else {
return Err(ModuleCapabilityError::EmptyHermeticFixtures);
};
let target = first.target.clone();
let input_digest = first.input_digest.clone();
if fixtures
.iter()
.any(|fixture| fixture.target != target || fixture.input_digest != input_digest)
{
return Err(ModuleCapabilityError::MixedHermeticFixtureInputs);
}
let output_digests = fixtures
.iter()
.map(|fixture| fixture.output_digest.clone())
.collect::<BTreeSet<_>>();
if output_digests.len() != 1 {
return Err(ModuleCapabilityError::NonHermeticCapabilityFixture {
target,
input_digest,
output_digests: output_digests.into_iter().collect(),
});
}
Ok(HermeticCapabilityReport {
target,
input_digest,
output_digest: output_digests.into_iter().next().unwrap_or_default(),
hosts: fixtures
.iter()
.map(|fixture| fixture.host.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HermeticCapabilityReport {
pub target: String,
pub input_digest: String,
pub output_digest: String,
pub hosts: Vec<String>,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ModuleCapabilityError {
#[error("no module targets are registered; load an external target module")]
NoTargets,
#[error(
"duplicate module target '{target}' from modules '{first_module}' and '{second_module}'"
)]
DuplicateTarget {
target: String,
first_module: String,
second_module: String,
},
#[error("unknown module target '{target}'; available targets: {}", available.join(", "))]
UnknownTarget {
target: String,
available: Vec<String>,
},
#[error("no default module target is registered; available targets: {}", available.join(", "))]
NoDefaultTarget {
available: Vec<String>,
},
#[error("multiple default module targets are registered: {}", targets.join(", "))]
MultipleDefaultTargets {
targets: Vec<String>,
},
#[error("module target '{target}' requested unavailable host imports: {}", denied.join(", "))]
DeniedHostImports {
target: String,
denied: Vec<String>,
},
#[error(
"module target '{target}' requires incompatible capability contract: {}",
diagnostic_codes.join(", ")
)]
IncompatibleCapabilityContract {
target: String,
diagnostic_codes: Vec<String>,
},
#[error(
"module target '{target}' requested unavailable resource handles: {}",
denied.join(", ")
)]
DeniedResourceHandles {
target: String,
denied: Vec<String>,
},
#[error("no hermetic capability fixtures were provided")]
EmptyHermeticFixtures,
#[error("hermetic capability fixtures must use one target and input digest")]
MixedHermeticFixtureInputs,
#[error("module target '{target}' is not hermetic for input {input_digest}")]
NonHermeticCapabilityFixture {
target: String,
input_digest: String,
output_digests: Vec<String>,
},
}
fn sorted_unique(values: &[String]) -> Vec<String> {
values
.iter()
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}