Skip to main content

plecto_host/
conformance.rs

1//! Generic conformance checks for a `plecto:filter` component (ADR 000065): properties any
2//! conformant filter can be expected to have, NOT a specific fixture's policy (the
3//! `filter-hello` family's `x-plecto-block` header etc., exercised by `tests/polyglot.rs` as
4//! an internal Rust regression suite). `plecto conformance <component.wasm>` is the CLI
5//! surface over [`check`]. See host/CONTEXT.md "Conformant (component)".
6//!
7//! Each run self-signs with a fresh, throwaway [`DevSigner`] key that is never persisted —
8//! NOT the same key `plecto dev` keeps at `.plecto/dev-key`. Since the CLI controls both the
9//! key and the signature it produces, a load-gate failure here can only mean the component
10//! itself is malformed (wrong world, bad export shape) — never a provenance problem, so
11//! "loads under the plecto:filter contract" already covers both structural and load-gate
12//! conformance in one observable check.
13
14use crate::dev_signer::{DevSigner, bound_sbom};
15use crate::{Header, Host, HttpRequest, LoadOptions, LoadedFilter, RequestTrace, SignedArtifact};
16
17/// One named property, pass/fail, with a human-readable detail (the failure reason, or a short
18/// confirmation on success).
19pub struct ConformanceCheck {
20    pub name: &'static str,
21    pub passed: bool,
22    pub detail: String,
23}
24
25/// The full battery's result. `plecto conformance` exits non-zero unless
26/// [`ConformanceReport::is_conformant`] is true.
27pub struct ConformanceReport {
28    pub checks: Vec<ConformanceCheck>,
29}
30
31impl ConformanceReport {
32    pub fn is_conformant(&self) -> bool {
33        self.checks.iter().all(|c| c.passed)
34    }
35}
36
37/// Run the generic conformance battery against raw component bytes.
38pub fn check(component_bytes: &[u8]) -> ConformanceReport {
39    let mut checks = Vec::new();
40
41    let loaded = load_self_signed(component_bytes);
42    checks.push(ConformanceCheck {
43        name: "loads under the plecto:filter contract",
44        passed: loaded.is_ok(),
45        detail: match &loaded {
46            Ok(_) => "component/SBOM self-signature verified, world satisfied".to_string(),
47            Err(e) => format!("{e:#}"),
48        },
49    });
50
51    let Ok(filter) = loaded else {
52        checks.push(ConformanceCheck {
53            name: "handles a generic request without trapping or exceeding its deadline",
54            passed: false,
55            detail: "skipped: component did not load".to_string(),
56        });
57        return ConformanceReport { checks };
58    };
59
60    let outcome = filter.on_request(&generic_request(), &RequestTrace::root());
61    checks.push(ConformanceCheck {
62        name: "handles a generic request without trapping or exceeding its deadline",
63        passed: outcome.is_ok(),
64        detail: match &outcome {
65            Ok((decision, _logs)) => format!("responded with {}", decision_kind(decision)),
66            Err(e) => e.to_string(),
67        },
68    });
69
70    ConformanceReport { checks }
71}
72
73fn load_self_signed(component_bytes: &[u8]) -> anyhow::Result<LoadedFilter> {
74    let (signer, _private_key_pem) = DevSigner::generate()?;
75    let component_signature = signer.sign(component_bytes)?;
76    let sbom = bound_sbom(component_bytes);
77    let sbom_signature = signer.sign(&sbom)?;
78    let host = Host::new(signer.trust_policy()?)?;
79    let artifact = SignedArtifact {
80        component_bytes,
81        component_signature: &component_signature,
82        sbom: &sbom,
83        sbom_signature: &sbom_signature,
84    };
85    // `Untrusted` (fresh-per-request, tight default deadlines): the conservative assumption for
86    // an arbitrary component this CLI has never seen before.
87    host.load("conformance", &artifact, LoadOptions::untrusted())
88}
89
90fn generic_request() -> HttpRequest {
91    HttpRequest {
92        method: "GET".to_string(),
93        path: "/".to_string(),
94        authority: "conformance.invalid".to_string(),
95        scheme: "https".to_string(),
96        headers: vec![Header {
97            name: "user-agent".to_string(),
98            value: b"plecto-conformance".to_vec(),
99        }],
100    }
101}
102
103fn decision_kind(decision: &crate::RequestDecision) -> &'static str {
104    match decision {
105        crate::RequestDecision::Continue => "continue",
106        crate::RequestDecision::Modified(_) => "modified",
107        crate::RequestDecision::ShortCircuit(_) => "short-circuit",
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn a_real_filter_is_conformant() {
117        let component = crate::test_support::filter_hello_component();
118        let report = check(&component);
119        for c in &report.checks {
120            assert!(c.passed, "{}: {}", c.name, c.detail);
121        }
122        assert!(report.is_conformant());
123    }
124
125    #[test]
126    fn garbage_bytes_fail_the_load_check() {
127        let report = check(b"not a wasm component");
128        assert!(!report.is_conformant());
129        assert!(!report.checks[0].passed);
130        assert_eq!(
131            report.checks.len(),
132            2,
133            "the runtime check is skipped, not silently dropped"
134        );
135        assert!(!report.checks[1].passed);
136        assert!(report.checks[1].detail.contains("skipped"));
137    }
138}