Skip to main content

bvisor/contract/
support.rs

1//! Static family support ([`SupportMatrix`]) vs RAW probe
2//! ([`BackendProfileSnapshot`]) vs TYPED planning view ([`BackendProfile`]).
3//!
4//! Three layers, deliberately separated so a string probe fact never becomes a
5//! security decision:
6//! - [`SupportMatrix`] is what a backend FAMILY can THEORETICALLY do — static,
7//!   per-backend-kind, TYPED.
8//! - [`BackendProfileSnapshot`] is RAW probe facts for THIS machine — portable,
9//!   string-ish, persisted in the plan + report for AUDIT/REPLAY ONLY. It is
10//!   NEVER consulted directly at admission.
11//! - [`BackendProfile`] is the TYPED planning view, derived DETERMINISTICALLY
12//!   from the raw snapshot so replay re-derives identical admission decisions.
13
14use crate::contract::budget::BudgetProfile;
15use crate::contract::capability::{Capability, SupportVerdict};
16use crate::contract::host_control::HostControl;
17use crate::contract::ids::BackendId;
18use crate::contract::plan::BoundaryRequirement;
19use batpak_macros::AllVariants;
20use serde::{Deserialize, Serialize};
21use std::collections::BTreeMap;
22
23/// What a backend FAMILY can THEORETICALLY do. Static, per-backend-kind, TYPED.
24///
25/// A rule maps a requirement KIND to the BEST verdict the family could reach;
26/// the [`BackendProfile`] then floors that verdict to what THIS machine has.
27/// Modeling the matrix as a typed table (rather than a closure) keeps it
28/// inert, serializable-in-principle, and replay-stable.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct SupportMatrix {
31    /// Best-case [`SupportVerdict`] per [`RequirementKind`]. A kind absent from
32    /// the table is the fail-closed bottom ([`SupportVerdict::unsupported`]).
33    best_case: BTreeMap<RequirementKind, SupportVerdict>,
34}
35
36impl SupportMatrix {
37    /// Build a support matrix from an explicit best-case table. Any
38    /// [`RequirementKind`] not listed is the fail-closed bottom.
39    #[must_use]
40    pub fn from_best_case(best_case: BTreeMap<RequirementKind, SupportVerdict>) -> Self {
41        Self { best_case }
42    }
43
44    /// The FAMILY best-case verdict for one [`RequirementKind`], independent of
45    /// any machine profile — the fail-closed bottom if the kind is absent.
46    ///
47    /// This is the static honesty surface: it exposes exactly what the family
48    /// CLAIMS it could do, so a per-platform honesty test (and the gauntlet's
49    /// lying-table red fixture) can assert a load-bearing `Unsupported`/`Mediated`
50    /// cell WITHOUT fabricating a machine profile. It is NOT consulted at
51    /// admission — `classify` floors this by the machine ceiling.
52    #[must_use]
53    pub fn best_case_for(&self, kind: RequirementKind) -> SupportVerdict {
54        self.best_case
55            .get(&kind)
56            .cloned()
57            .unwrap_or_else(SupportVerdict::unsupported)
58    }
59
60    /// The requirement kinds this matrix EXPLICITLY declares a best-case verdict
61    /// for, in canonical order. UNLIKE [`Self::best_case_for`] (which folds a
62    /// missing key into the fail-closed `Unsupported` bottom), this exposes the
63    /// LITERAL key set — so a completeness gate can tell an EXPLICIT `Unsupported`
64    /// claim (key present) from a SILENT GAP (key absent). The §2 law forbids a
65    /// silent gap: every key must carry a stated answer per backend.
66    #[must_use]
67    pub fn declared_kinds(&self) -> Vec<RequirementKind> {
68        self.best_case.keys().copied().collect()
69    }
70
71    /// Whether this matrix EXPLICITLY declares a verdict for `kind` (a stated
72    /// answer, even if that answer is `Unsupported`). A `false` is a SILENT GAP —
73    /// the exact completeness violation the per-profile gate flags.
74    #[must_use]
75    pub fn declares(&self, kind: RequirementKind) -> bool {
76        self.best_case.contains_key(&kind)
77    }
78
79    /// Classify a requirement against the TYPED profile (no string parsing at
80    /// admission). The verdict is the family best-case MET with what the machine
81    /// profile actually provides — enforcement floored, evidence intersected.
82    #[must_use]
83    pub fn classify(&self, req: &BoundaryRequirement, profile: &BackendProfile) -> SupportVerdict {
84        let kind = RequirementKind::of(req);
85        let best = self
86            .best_case
87            .get(&kind)
88            .cloned()
89            .unwrap_or_else(SupportVerdict::unsupported);
90        best.meet(&profile.ceiling_for(kind))
91    }
92}
93
94/// The classification key: a requirement's CANONICAL POLICY identity (proof-spine
95/// §2), one key per semantically-distinct policy.
96///
97/// Guarantee-shaped, not mechanism-shaped — the matrix grades the KIND of thing
98/// asked. The key is INJECTIVE over canonical-policy meaning (the §2 law: distinct
99/// [`crate::contract::canonical_policy::CanonicalPolicy`] ⇒ distinct key), so each
100/// capability variant that carries a distinct policy gets its OWN key
101/// (`Network { DenyAll }` vs `Network { AllowList }`, `InheritedFds { None }` vs
102/// `{ Only }`, the three `ChildSpawn` child-task semantics). A future backend could
103/// differentiate cells we currently lower identically, so we never pre-collapse
104/// them. `Environment` carries a single policy variant (`Exact`), so it is one key
105/// (the per-entry name/value/source detail rides the canonical-policy PAYLOAD, not
106/// the variant-level key).
107#[derive(
108    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, AllVariants,
109)]
110#[non_exhaustive]
111pub enum RequirementKind {
112    /// [`Capability::Filesystem`].
113    Filesystem,
114    /// [`Capability::Network`] with `DenyAll`.
115    NetworkDenyAll,
116    /// [`Capability::Network`] with `AllowList`.
117    NetworkAllowList,
118    /// [`Capability::ChildSpawn`] with [`crate::SpawnPolicy::DenyNewTasks`].
119    ChildSpawnDenyNewTasks,
120    /// [`Capability::ChildSpawn`] with [`crate::SpawnPolicy::AllowThreadsWithinBoundary`].
121    ChildSpawnAllowThreads,
122    /// [`Capability::ChildSpawn`] with [`crate::SpawnPolicy::AllowDescendantsWithinBoundary`].
123    ChildSpawnAllowDescendants,
124    /// [`Capability::Environment`].
125    Environment,
126    /// [`Capability::InheritedFds`] with [`crate::FdPolicy::None`].
127    InheritedFdsNone,
128    /// [`Capability::InheritedFds`] with [`crate::FdPolicy::Only`].
129    InheritedFdsOnly,
130    /// [`HostControl::LaunchWorkload`].
131    LaunchWorkload,
132    /// [`HostControl::CaptureStreams`].
133    CaptureStreams,
134    /// [`HostControl::TempRoot`].
135    TempRoot,
136    /// [`HostControl::ExposePath`].
137    ExposePath,
138    /// [`HostControl::CommitArtifact`].
139    CommitArtifact,
140    /// [`HostControl::DiscardArtifact`].
141    DiscardArtifact,
142    /// [`HostControl::Kill`].
143    Kill,
144    /// [`HostControl::ListOutputs`].
145    ListOutputs,
146}
147
148impl RequirementKind {
149    /// Derive the classification key from a concrete requirement.
150    #[must_use]
151    pub fn of(req: &BoundaryRequirement) -> Self {
152        match req {
153            BoundaryRequirement::Capability(cap) => Self::of_capability(cap),
154            BoundaryRequirement::HostControl(ctrl) => Self::of_control(ctrl),
155        }
156    }
157
158    /// Derive the policy-aware key (proof-spine §2): each capability's distinct
159    /// CANONICAL POLICY maps to a distinct key. POLICY-AWARE for ALL kinds — no
160    /// policy-blind collapse — so two semantically-distinct policies under the same
161    /// capability variant (`InheritedFds::None` vs `::Only`, the three `ChildSpawn`
162    /// child-task semantics, `Network::DenyAll` vs `::AllowList`) never share a key.
163    /// `Environment` has a single policy variant (`Exact`), so one key.
164    fn of_capability(cap: &Capability) -> Self {
165        use crate::contract::capability::{FdPolicy, NetPolicy, SpawnPolicy};
166        match cap {
167            Capability::Filesystem { .. } => Self::Filesystem,
168            Capability::Network {
169                policy: NetPolicy::DenyAll,
170            } => Self::NetworkDenyAll,
171            Capability::Network {
172                policy: NetPolicy::AllowList(_),
173            } => Self::NetworkAllowList,
174            Capability::ChildSpawn {
175                policy: SpawnPolicy::DenyNewTasks,
176            } => Self::ChildSpawnDenyNewTasks,
177            Capability::ChildSpawn {
178                policy: SpawnPolicy::AllowThreadsWithinBoundary,
179            } => Self::ChildSpawnAllowThreads,
180            Capability::ChildSpawn {
181                policy: SpawnPolicy::AllowDescendantsWithinBoundary,
182            } => Self::ChildSpawnAllowDescendants,
183            Capability::Environment { .. } => Self::Environment,
184            Capability::InheritedFds {
185                policy: FdPolicy::None,
186            } => Self::InheritedFdsNone,
187            Capability::InheritedFds {
188                policy: FdPolicy::Only(_),
189            } => Self::InheritedFdsOnly,
190        }
191    }
192
193    /// Test-only accessor for the policy→key map (the injective gate exercises it
194    /// directly with constructed capabilities). Production code reaches it through
195    /// [`Self::of`].
196    #[cfg(test)]
197    pub(crate) fn of_capability_for_test(cap: &Capability) -> Self {
198        Self::of_capability(cap)
199    }
200
201    fn of_control(ctrl: &HostControl) -> Self {
202        match ctrl {
203            HostControl::LaunchWorkload => Self::LaunchWorkload,
204            HostControl::CaptureStreams { .. } => Self::CaptureStreams,
205            HostControl::TempRoot { .. } => Self::TempRoot,
206            HostControl::ExposePath { .. } => Self::ExposePath,
207            HostControl::CommitArtifact { .. } => Self::CommitArtifact,
208            HostControl::DiscardArtifact => Self::DiscardArtifact,
209            HostControl::Kill { .. } => Self::Kill,
210            HostControl::ListOutputs => Self::ListOutputs,
211        }
212    }
213}
214
215/// RAW probe facts — portable, string-ish, persisted in the plan + report for
216/// AUDIT/REPLAY ONLY. NEVER consulted directly at admission.
217#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
218pub struct BackendProfileSnapshot {
219    /// The backend this snapshot was probed from.
220    pub backend: BackendId,
221    /// Raw probe facts, e.g. `"landlock_abi" -> "4"`, `"cgroup_v2" -> "true"`.
222    /// A `BTreeMap` so the persisted bytes are key-sorted and replay-stable.
223    pub probed: BTreeMap<String, String>,
224    /// The machine's seven-dimensional budget capability — backend-declared
225    /// availability, guarantee, evidence, and mechanism per dimension. Bound into
226    /// plan identity via `H_P`; never authored by the caller.
227    pub budget: BudgetProfile,
228}
229
230/// TYPED planning view, derived DETERMINISTICALLY from a raw snapshot. The
231/// planner consults THIS, never the map.
232///
233/// Holds a per-[`RequirementKind`] machine CEILING: the strongest enforcement
234/// the machine can actually back for that kind right now.
235#[derive(Clone, Debug, PartialEq, Eq)]
236pub struct BackendProfile {
237    ceiling: BTreeMap<RequirementKind, SupportVerdict>,
238}
239
240impl BackendProfile {
241    /// Build a typed profile from an explicit per-kind ceiling table. A kind
242    /// absent from the table is the fail-closed bottom.
243    #[must_use]
244    pub fn from_ceiling(ceiling: BTreeMap<RequirementKind, SupportVerdict>) -> Self {
245        Self { ceiling }
246    }
247
248    /// The machine ceiling [`SupportVerdict`] for one requirement kind; the
249    /// fail-closed bottom if unknown.
250    #[must_use]
251    pub fn ceiling_for(&self, kind: RequirementKind) -> SupportVerdict {
252        self.ceiling
253            .get(&kind)
254            .cloned()
255            .unwrap_or_else(SupportVerdict::unsupported)
256    }
257
258    /// The requirement kinds this ceiling advertises at
259    /// [`Enforcement::Enforced`](crate::contract::capability::Enforcement::Enforced),
260    /// in canonical order — exactly the cells the qualification coupling gate must
261    /// find a `Proven` ledger row for.
262    #[must_use]
263    pub fn enforced_kinds(&self) -> Vec<RequirementKind> {
264        RequirementKind::ALL
265            .into_iter()
266            .filter(|&k| {
267                self.ceiling_for(k).enforcement
268                    == crate::contract::capability::Enforcement::Enforced
269            })
270            .collect()
271    }
272}
273
274#[cfg(test)]
275mod requirement_kind_tests {
276    use super::RequirementKind;
277
278    /// `RequirementKind::ALL` must list EVERY variant (the coupling gate enumerates
279    /// it). A new variant added without extending `ALL` makes this `match` fail to
280    /// compile — the exhaustiveness tripwire — and the count assert backs it up.
281    #[test]
282    fn all_is_exhaustive() {
283        for kind in RequirementKind::ALL {
284            // Exhaustive match: a new variant forces this to be updated.
285            match kind {
286                RequirementKind::Filesystem
287                | RequirementKind::NetworkDenyAll
288                | RequirementKind::NetworkAllowList
289                | RequirementKind::ChildSpawnDenyNewTasks
290                | RequirementKind::ChildSpawnAllowThreads
291                | RequirementKind::ChildSpawnAllowDescendants
292                | RequirementKind::Environment
293                | RequirementKind::InheritedFdsNone
294                | RequirementKind::InheritedFdsOnly
295                | RequirementKind::LaunchWorkload
296                | RequirementKind::CaptureStreams
297                | RequirementKind::TempRoot
298                | RequirementKind::ExposePath
299                | RequirementKind::CommitArtifact
300                | RequirementKind::DiscardArtifact
301                | RequirementKind::Kill
302                | RequirementKind::ListOutputs => {}
303            }
304        }
305        // No duplicates.
306        let mut seen = std::collections::BTreeSet::new();
307        for kind in RequirementKind::ALL {
308            assert!(seen.insert(kind), "duplicate kind in ALL: {kind:?}");
309        }
310        assert_eq!(seen.len(), RequirementKind::ALL.len());
311    }
312}
313
314#[cfg(test)]
315#[path = "support_injective_tests.rs"]
316mod support_injective_tests;