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;