1use std::{
8 collections::{BTreeMap, BTreeSet},
9 fmt,
10};
11
12pub const WESLEY_CAPABILITY_ABI: &str = "wesley-capability-abi";
14
15pub const CURRENT_CAPABILITY_ABI_VERSION: CapabilityContractVersion =
17 CapabilityContractVersion::new(0, 1, 0);
18
19#[derive(
21 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
22)]
23#[serde(rename_all = "camelCase")]
24pub struct CapabilityContractVersion {
25 pub major: u64,
27 pub minor: u64,
29 pub patch: u64,
31}
32
33impl CapabilityContractVersion {
34 pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
36 Self {
37 major,
38 minor,
39 patch,
40 }
41 }
42}
43
44impl fmt::Display for CapabilityContractVersion {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct CapabilityVersionRequirement {
54 pub abi: String,
56 pub minimum: CapabilityContractVersion,
58 pub maximum_exclusive: CapabilityContractVersion,
60}
61
62impl CapabilityVersionRequirement {
63 pub fn new(
65 abi: impl Into<String>,
66 minimum: CapabilityContractVersion,
67 maximum_exclusive: CapabilityContractVersion,
68 ) -> Self {
69 Self {
70 abi: abi.into(),
71 minimum,
72 maximum_exclusive,
73 }
74 }
75
76 pub fn current() -> Self {
78 Self::new(
79 WESLEY_CAPABILITY_ABI,
80 CURRENT_CAPABILITY_ABI_VERSION,
81 CapabilityContractVersion::new(0, 2, 0),
82 )
83 }
84
85 pub fn allows(&self, abi: &str, version: CapabilityContractVersion) -> bool {
87 self.abi == abi && self.minimum <= version && version < self.maximum_exclusive
88 }
89}
90
91impl fmt::Display for CapabilityVersionRequirement {
92 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93 write!(
94 formatter,
95 "{} >={} <{}",
96 self.abi, self.minimum, self.maximum_exclusive
97 )
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
103#[serde(rename_all = "kebab-case")]
104pub enum CapabilityExecutionMode {
105 RustNative,
107 Wasm,
109 ExternalProcess,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
115#[serde(rename_all = "kebab-case")]
116pub enum CapabilityPortabilityFloor {
117 HostNative,
119 PortableWasm,
121 ExternalProcess,
123}
124
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
127#[serde(rename_all = "kebab-case")]
128pub enum CapabilityRuntimeModel {
129 #[default]
131 Stateless,
132 ResourceHandles,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct ModuleTargetDescriptor {
140 pub module: String,
142 pub target: String,
144 pub is_default: bool,
146 pub execution_mode: CapabilityExecutionMode,
148 pub portability_floor: CapabilityPortabilityFloor,
150 #[serde(default = "CapabilityVersionRequirement::current")]
152 pub required_contract: CapabilityVersionRequirement,
153 #[serde(default)]
155 pub runtime_model: CapabilityRuntimeModel,
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 pub requested_host_imports: Vec<String>,
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
161 pub requested_resource_handles: Vec<String>,
162}
163
164#[derive(Debug, Default, Clone, PartialEq, Eq)]
166pub struct ModuleTargetRegistry {
167 targets: BTreeMap<String, ModuleTargetDescriptor>,
168}
169
170impl ModuleTargetRegistry {
171 pub fn from_targets(
173 targets: impl IntoIterator<Item = ModuleTargetDescriptor>,
174 ) -> Result<Self, ModuleCapabilityError> {
175 let mut registry = Self::default();
176 for target in targets {
177 registry.register(target)?;
178 }
179
180 Ok(registry)
181 }
182
183 pub fn register(
185 &mut self,
186 target: ModuleTargetDescriptor,
187 ) -> Result<(), ModuleCapabilityError> {
188 if let Some(existing) = self.targets.get(&target.target) {
189 return Err(ModuleCapabilityError::DuplicateTarget {
190 target: target.target,
191 first_module: existing.module.clone(),
192 second_module: target.module,
193 });
194 }
195
196 self.targets.insert(target.target.clone(), target);
197 Ok(())
198 }
199
200 pub fn resolve_target(
202 &self,
203 requested: Option<&str>,
204 ) -> Result<&ModuleTargetDescriptor, ModuleCapabilityError> {
205 if self.targets.is_empty() {
206 return Err(ModuleCapabilityError::NoTargets);
207 }
208
209 if let Some(requested) = requested {
210 return self.targets.get(requested).ok_or_else(|| {
211 ModuleCapabilityError::UnknownTarget {
212 target: requested.to_string(),
213 available: self.targets.keys().cloned().collect(),
214 }
215 });
216 }
217
218 let defaults = self
219 .targets
220 .values()
221 .filter(|target| target.is_default)
222 .collect::<Vec<_>>();
223
224 match defaults.as_slice() {
225 [target] => Ok(target),
226 [] => Err(ModuleCapabilityError::NoDefaultTarget {
227 available: self.targets.keys().cloned().collect(),
228 }),
229 targets => Err(ModuleCapabilityError::MultipleDefaultTargets {
230 targets: targets.iter().map(|target| target.target.clone()).collect(),
231 }),
232 }
233 }
234
235 pub fn capability_report(
237 &self,
238 requested: impl IntoIterator<Item = impl AsRef<str>>,
239 ) -> CapabilityReport {
240 let mut requested_targets = Vec::new();
241 let mut granted = Vec::new();
242 let mut denied = Vec::new();
243
244 for target in requested {
245 let target = target.as_ref().to_string();
246 requested_targets.push(target.clone());
247 if self.targets.contains_key(&target) {
248 granted.push(target);
249 } else {
250 denied.push(target);
251 }
252 }
253
254 CapabilityReport {
255 requested: requested_targets,
256 granted,
257 denied,
258 }
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct CapabilityReport {
266 pub requested: Vec<String>,
268 pub granted: Vec<String>,
270 pub denied: Vec<String>,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct HostCapabilityContract {
278 pub host: String,
280 pub abi: String,
282 pub version: CapabilityContractVersion,
284}
285
286impl HostCapabilityContract {
287 pub fn current() -> Self {
289 Self {
290 host: "wesley-rust-host".to_string(),
291 abi: WESLEY_CAPABILITY_ABI.to_string(),
292 version: CURRENT_CAPABILITY_ABI_VERSION,
293 }
294 }
295
296 pub fn evaluate_contract(&self, target: &ModuleTargetDescriptor) -> CapabilityContractReport {
298 let mut diagnostics = Vec::new();
299
300 if target.required_contract.abi != self.abi {
301 diagnostics.push(CapabilityContractDiagnostic {
302 code: "MODULE_CONTRACT_ABI_MISMATCH".to_string(),
303 target: target.target.clone(),
304 host: self.host.clone(),
305 host_version: self.version.to_string(),
306 required: target.required_contract.to_string(),
307 });
308 } else if !target.required_contract.allows(&self.abi, self.version) {
309 diagnostics.push(CapabilityContractDiagnostic {
310 code: "WASM_ABI_UNSUPPORTED".to_string(),
311 target: target.target.clone(),
312 host: self.host.clone(),
313 host_version: self.version.to_string(),
314 required: target.required_contract.to_string(),
315 });
316 }
317
318 CapabilityContractReport {
319 target: target.target.clone(),
320 host: self.host.clone(),
321 host_version: self.version.to_string(),
322 required: target.required_contract.to_string(),
323 accepted: diagnostics.is_empty(),
324 diagnostics,
325 }
326 }
327
328 pub fn reject_incompatible_contract_before_execution(
330 &self,
331 target: &ModuleTargetDescriptor,
332 ) -> Result<CapabilityContractReport, ModuleCapabilityError> {
333 let report = self.evaluate_contract(target);
334 if report.accepted {
335 Ok(report)
336 } else {
337 Err(ModuleCapabilityError::IncompatibleCapabilityContract {
338 target: target.target.clone(),
339 diagnostic_codes: report
340 .diagnostics
341 .iter()
342 .map(|diagnostic| diagnostic.code.clone())
343 .collect(),
344 })
345 }
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct CapabilityContractReport {
353 pub target: String,
355 pub host: String,
357 pub host_version: String,
359 pub required: String,
361 pub accepted: bool,
363 pub diagnostics: Vec<CapabilityContractDiagnostic>,
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
369#[serde(rename_all = "camelCase")]
370pub struct CapabilityContractDiagnostic {
371 pub code: String,
373 pub target: String,
375 pub host: String,
377 pub host_version: String,
379 pub required: String,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
385pub struct HostFunctionPolicy {
386 profile: String,
387 allowed_imports: BTreeSet<String>,
388}
389
390impl HostFunctionPolicy {
391 pub fn pure() -> Self {
393 Self {
394 profile: "pure".to_string(),
395 allowed_imports: BTreeSet::new(),
396 }
397 }
398
399 pub fn allowing(
401 profile: impl Into<String>,
402 imports: impl IntoIterator<Item = impl Into<String>>,
403 ) -> Self {
404 Self {
405 profile: profile.into(),
406 allowed_imports: imports.into_iter().map(Into::into).collect(),
407 }
408 }
409
410 pub fn evaluate(&self, target: &ModuleTargetDescriptor) -> HostImportReport {
412 let requested = sorted_unique(&target.requested_host_imports);
413 let mut granted = Vec::new();
414 let mut denied = Vec::new();
415
416 for requested_import in &requested {
417 if self.allowed_imports.contains(requested_import) {
418 granted.push(requested_import.clone());
419 } else {
420 denied.push(requested_import.clone());
421 }
422 }
423
424 HostImportReport {
425 profile: self.profile.clone(),
426 target: target.target.clone(),
427 requested,
428 granted,
429 denied,
430 }
431 }
432
433 pub fn reject_unavailable_imports_before_execution(
435 &self,
436 target: &ModuleTargetDescriptor,
437 ) -> Result<HostImportReport, ModuleCapabilityError> {
438 let report = self.evaluate(target);
439 if report.denied.is_empty() {
440 Ok(report)
441 } else {
442 Err(ModuleCapabilityError::DeniedHostImports {
443 target: target.target.clone(),
444 denied: report.denied,
445 })
446 }
447 }
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct HostImportReport {
454 pub profile: String,
456 pub target: String,
458 pub requested: Vec<String>,
460 pub granted: Vec<String>,
462 pub denied: Vec<String>,
464}
465
466#[derive(Debug, Clone, PartialEq, Eq)]
468pub struct RuntimeResourcePolicy {
469 model: CapabilityRuntimeModel,
470 allowed_resource_handles: BTreeSet<String>,
471}
472
473impl RuntimeResourcePolicy {
474 pub fn stateless_default() -> Self {
476 Self {
477 model: CapabilityRuntimeModel::Stateless,
478 allowed_resource_handles: BTreeSet::new(),
479 }
480 }
481
482 pub fn allowing_resource_handles(handles: impl IntoIterator<Item = impl Into<String>>) -> Self {
484 Self {
485 model: CapabilityRuntimeModel::ResourceHandles,
486 allowed_resource_handles: handles.into_iter().map(Into::into).collect(),
487 }
488 }
489
490 pub fn evaluate(&self, target: &ModuleTargetDescriptor) -> RuntimeResourceReport {
492 let requested = sorted_unique(&target.requested_resource_handles);
493 let mut granted = Vec::new();
494 let mut denied = Vec::new();
495
496 for requested_handle in &requested {
497 if self.model == CapabilityRuntimeModel::ResourceHandles
498 && self.allowed_resource_handles.contains(requested_handle)
499 {
500 granted.push(requested_handle.clone());
501 } else {
502 denied.push(requested_handle.clone());
503 }
504 }
505
506 RuntimeResourceReport {
507 model: self.model,
508 target: target.target.clone(),
509 requested,
510 granted,
511 denied,
512 }
513 }
514
515 pub fn reject_resource_handles_before_execution(
517 &self,
518 target: &ModuleTargetDescriptor,
519 ) -> Result<RuntimeResourceReport, ModuleCapabilityError> {
520 let report = self.evaluate(target);
521 if report.denied.is_empty() {
522 Ok(report)
523 } else {
524 Err(ModuleCapabilityError::DeniedResourceHandles {
525 target: target.target.clone(),
526 denied: report.denied,
527 })
528 }
529 }
530}
531
532#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct RuntimeResourceReport {
536 pub model: CapabilityRuntimeModel,
538 pub target: String,
540 pub requested: Vec<String>,
542 pub granted: Vec<String>,
544 pub denied: Vec<String>,
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
550#[serde(rename_all = "camelCase")]
551pub struct HermeticCapabilityFixture {
552 pub host: String,
554 pub target: String,
556 pub input_digest: String,
558 pub output_digest: String,
560}
561
562impl HermeticCapabilityFixture {
563 pub fn new(
565 host: impl Into<String>,
566 target: impl Into<String>,
567 input_digest: impl Into<String>,
568 output_digest: impl Into<String>,
569 ) -> Self {
570 Self {
571 host: host.into(),
572 target: target.into(),
573 input_digest: input_digest.into(),
574 output_digest: output_digest.into(),
575 }
576 }
577
578 pub fn verify_cross_host_outputs(
580 fixtures: impl IntoIterator<Item = HermeticCapabilityFixture>,
581 ) -> Result<HermeticCapabilityReport, ModuleCapabilityError> {
582 let fixtures = fixtures.into_iter().collect::<Vec<_>>();
583 let Some(first) = fixtures.first() else {
584 return Err(ModuleCapabilityError::EmptyHermeticFixtures);
585 };
586
587 let target = first.target.clone();
588 let input_digest = first.input_digest.clone();
589
590 if fixtures
591 .iter()
592 .any(|fixture| fixture.target != target || fixture.input_digest != input_digest)
593 {
594 return Err(ModuleCapabilityError::MixedHermeticFixtureInputs);
595 }
596
597 let output_digests = fixtures
598 .iter()
599 .map(|fixture| fixture.output_digest.clone())
600 .collect::<BTreeSet<_>>();
601
602 if output_digests.len() != 1 {
603 return Err(ModuleCapabilityError::NonHermeticCapabilityFixture {
604 target,
605 input_digest,
606 output_digests: output_digests.into_iter().collect(),
607 });
608 }
609
610 Ok(HermeticCapabilityReport {
611 target,
612 input_digest,
613 output_digest: output_digests.into_iter().next().unwrap_or_default(),
614 hosts: fixtures
615 .iter()
616 .map(|fixture| fixture.host.clone())
617 .collect::<BTreeSet<_>>()
618 .into_iter()
619 .collect(),
620 })
621 }
622}
623
624#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
626#[serde(rename_all = "camelCase")]
627pub struct HermeticCapabilityReport {
628 pub target: String,
630 pub input_digest: String,
632 pub output_digest: String,
634 pub hosts: Vec<String>,
636}
637
638#[derive(Debug, thiserror::Error, PartialEq, Eq)]
640pub enum ModuleCapabilityError {
641 #[error("no module targets are registered; load an external target module")]
643 NoTargets,
644 #[error(
646 "duplicate module target '{target}' from modules '{first_module}' and '{second_module}'"
647 )]
648 DuplicateTarget {
649 target: String,
651 first_module: String,
653 second_module: String,
655 },
656 #[error("unknown module target '{target}'; available targets: {}", available.join(", "))]
658 UnknownTarget {
659 target: String,
661 available: Vec<String>,
663 },
664 #[error("no default module target is registered; available targets: {}", available.join(", "))]
666 NoDefaultTarget {
667 available: Vec<String>,
669 },
670 #[error("multiple default module targets are registered: {}", targets.join(", "))]
672 MultipleDefaultTargets {
673 targets: Vec<String>,
675 },
676 #[error("module target '{target}' requested unavailable host imports: {}", denied.join(", "))]
678 DeniedHostImports {
679 target: String,
681 denied: Vec<String>,
683 },
684 #[error(
686 "module target '{target}' requires incompatible capability contract: {}",
687 diagnostic_codes.join(", ")
688 )]
689 IncompatibleCapabilityContract {
690 target: String,
692 diagnostic_codes: Vec<String>,
694 },
695 #[error(
697 "module target '{target}' requested unavailable resource handles: {}",
698 denied.join(", ")
699 )]
700 DeniedResourceHandles {
701 target: String,
703 denied: Vec<String>,
705 },
706 #[error("no hermetic capability fixtures were provided")]
708 EmptyHermeticFixtures,
709 #[error("hermetic capability fixtures must use one target and input digest")]
711 MixedHermeticFixtureInputs,
712 #[error("module target '{target}' is not hermetic for input {input_digest}")]
714 NonHermeticCapabilityFixture {
715 target: String,
717 input_digest: String,
719 output_digests: Vec<String>,
721 },
722}
723
724fn sorted_unique(values: &[String]) -> Vec<String> {
725 values
726 .iter()
727 .cloned()
728 .collect::<BTreeSet<_>>()
729 .into_iter()
730 .collect()
731}