use crate::contract::admission::{planner_shadow_check, AdmissionOutcome, PlannerInputs};
use crate::contract::backend::Backend;
use crate::contract::budget::{budget_admit, AdmittedBudgets, DerivedMinimums};
use crate::contract::capability::{
Capability, Enforcement, EvidenceClaim, EvidenceSet, FdPolicy, FsAccess, NetPolicy,
SpawnPolicy, SupportVerdict,
};
use crate::contract::host_control::HostControl;
use crate::contract::ids::{BackendId, BoundaryPlanHash, Digest32};
use crate::contract::plan::{
AdmittedRequirement, BoundaryPlan, BoundaryRequirement, BoundarySpec, PlanError,
BOUNDARY_PLAN_SCHEMA_VERSION,
};
use crate::contract::report::{BoundaryReport, BoundaryReportBody, ObservedFact};
use crate::contract::support::BackendProfileSnapshot;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Clone, Default)]
pub struct BackendRegistry {
backends: BTreeMap<BackendId, Arc<dyn Backend>>,
}
impl BackendRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, backend: Arc<dyn Backend>) {
self.backends.insert(backend.id(), backend);
}
#[must_use]
pub fn backend(&self, id: &BackendId) -> Option<&Arc<dyn Backend>> {
self.backends.get(id)
}
}
pub struct BoundaryPlanner<'r> {
registry: &'r BackendRegistry,
}
impl<'r> BoundaryPlanner<'r> {
#[must_use]
pub fn new(registry: &'r BackendRegistry) -> Self {
Self { registry }
}
pub fn plan(
&self,
spec: &BoundarySpec,
backend: &BackendId,
) -> Result<BoundaryPlan, PlanError> {
let backend = self
.registry
.backend(backend)
.ok_or_else(|| PlanError::UnknownBackend {
backend: backend.clone(),
})?;
validate_capability_policies(&spec.capabilities)?;
let snapshot = backend.probe();
let profile = backend.profile(&snapshot);
let mut classified: Vec<(BoundaryRequirement, SupportVerdict)> = Vec::new();
for capability in &spec.capabilities {
let req = BoundaryRequirement::Capability(capability.clone());
let verdict = backend.classify(&req, &profile);
classified.push((req, verdict));
}
for control in &spec.controls {
let req = BoundaryRequirement::HostControl(control.clone());
let verdict = backend.classify(&req, &profile);
classified.push((req, verdict));
}
let required = spec.evidence.required_claims();
let mut available = EvidenceSet::new();
for (_, verdict) in &classified {
available.extend_from(&verdict.evidence);
}
let inputs = PlannerInputs {
enforcement: classified
.iter()
.map(|(_, verdict)| enforcement_code(verdict.enforcement))
.collect(),
evidence_required: evidence_bits(&required),
evidence_available: evidence_bits(&available),
};
let outcome =
planner_shadow_check(&inputs).map_err(|divergence| PlanError::ShadowDivergence {
detail: divergence.to_string(),
})?;
match outcome {
AdmissionOutcome::Admitted { .. } => {
build_admitted_plan(backend.as_ref(), snapshot, &classified, spec)
}
AdmissionOutcome::Refused { membrane: 1, .. } => {
match classified
.iter()
.find(|(_, verdict)| verdict.enforcement == Enforcement::Unsupported)
{
Some((requirement, _)) => Err(PlanError::Unsupported {
requirement: requirement.clone(),
backend: backend.id(),
}),
None => Err(PlanError::ShadowDivergence {
detail: "support refusal without an unsupported requirement".to_string(),
}),
}
}
AdmissionOutcome::Refused { membrane: 2, .. } => Err(PlanError::EvidenceUnsatisfiable {
backend: backend.id(),
detail: format!(
"required evidence {required:?} is not a subset of admitted evidence {available:?}"
),
}),
AdmissionOutcome::Refused { membrane, .. } => Err(PlanError::ShadowDivergence {
detail: format!("unexpected refusal membrane {membrane}"),
}),
}
}
}
fn build_admitted_plan(
backend: &dyn Backend,
snapshot: BackendProfileSnapshot,
classified: &[(BoundaryRequirement, SupportVerdict)],
spec: &BoundarySpec,
) -> Result<BoundaryPlan, PlanError> {
let derived = derive_minimums(spec);
let profile_digest =
budget_profile_digest(&snapshot).map_err(|error| PlanError::ProfileInsufficient {
backend: backend.id(),
detail: format!("budget profile canonicalization failed: {error}"),
})?;
let admitted_budgets = budget_admit(&spec.budgets, &snapshot.budget, &derived, profile_digest)
.map_err(|refusal| PlanError::BudgetRefused {
backend: backend.id(),
dimension: refusal.dimension,
failure: refusal.failure,
})?;
let admitted: Vec<AdmittedRequirement> = classified
.iter()
.map(|(requirement, verdict)| AdmittedRequirement {
mechanism: backend.mechanism(requirement, verdict.enforcement),
requirement: requirement.clone(),
enforcement: verdict.enforcement,
})
.collect();
let plan_id = compute_plan_id(backend.id(), &snapshot, &admitted, spec, &admitted_budgets)
.map_err(|error| PlanError::ProfileInsufficient {
backend: backend.id(),
detail: format!("plan canonicalization failed: {error}"),
})?;
Ok(BoundaryPlan {
schema_version: BOUNDARY_PLAN_SCHEMA_VERSION,
plan_id,
backend: backend.id(),
profile: snapshot,
admitted,
workload: spec.workload.clone(),
budgets: admitted_budgets,
evidence: spec.evidence,
})
}
fn validate_capability_policies(capabilities: &[Capability]) -> Result<(), PlanError> {
for capability in capabilities {
if let Capability::Environment { policy } = capability {
policy
.validate()
.map_err(|error| PlanError::InvalidPolicy {
detail: error.to_string(),
})?;
}
}
Ok(())
}
fn enforcement_code(enforcement: Enforcement) -> u8 {
match enforcement {
Enforcement::Unsupported => 0,
Enforcement::Mediated => 1,
Enforcement::Enforced => 2,
}
}
fn evidence_bit(claim: EvidenceClaim) -> u32 {
match claim {
EvidenceClaim::TerminalOutcome => 0,
EvidenceClaim::CapturedStreams => 1,
EvidenceClaim::ResourceUsage => 2,
EvidenceClaim::AllowedActions => 3,
EvidenceClaim::DeniedAttempts => 4,
EvidenceClaim::FilesystemDelta => 5,
EvidenceClaim::ProcessTree => 6,
EvidenceClaim::NetworkActivity => 7,
EvidenceClaim::ArtifactLineage => 8,
EvidenceClaim::MechanismAttestation => 9,
}
}
fn evidence_bits(set: &EvidenceSet) -> u16 {
let mut bits = 0u16;
for claim in set.iter() {
bits |= 1u16 << evidence_bit(claim);
}
bits
}
#[must_use]
pub fn derive_minimums(spec: &BoundarySpec) -> DerivedMinimums {
let mut minimums = DerivedMinimums::default();
let launches = spec
.controls
.iter()
.any(|control| matches!(control, HostControl::LaunchWorkload));
if launches {
minimums.process_count = 1;
minimums.wall_micros = 1;
minimums.cpu_micros = 1;
minimums.resident_bytes = 1;
minimums.handle_count = 3;
}
for control in &spec.controls {
if let HostControl::CaptureStreams { streams } = control {
minimums.handle_count +=
u64::from(streams.stdout) + u64::from(streams.stderr) + u64::from(streams.stdin);
}
if matches!(
control,
HostControl::TempRoot { .. } | HostControl::CommitArtifact { .. }
) {
minimums.storage_bytes = minimums.storage_bytes.max(1);
}
if let HostControl::ExposePath { access, .. } = control {
if matches!(access, FsAccess::Write | FsAccess::ReadWrite) {
minimums.storage_bytes = minimums.storage_bytes.max(1);
}
}
}
for capability in &spec.capabilities {
if matches!(
capability,
Capability::ChildSpawn {
policy: SpawnPolicy::AllowThreadsWithinBoundary
| SpawnPolicy::AllowDescendantsWithinBoundary
}
) {
minimums.process_count = minimums.process_count.max(2);
}
if let Capability::InheritedFds {
policy: FdPolicy::Only(fds),
} = capability
{
minimums.handle_count += u64::try_from(fds.len()).unwrap_or(u64::MAX);
}
if matches!(
capability,
Capability::Network {
policy: NetPolicy::AllowList(_)
}
) {
minimums.network_bytes = minimums.network_bytes.max(1);
}
}
minimums
}
fn compute_plan_id(
backend: BackendId,
snapshot: &BackendProfileSnapshot,
admitted: &[AdmittedRequirement],
spec: &BoundarySpec,
admitted_budgets: &AdmittedBudgets,
) -> Result<BoundaryPlanHash, rmp_serde::encode::Error> {
let mut sorted_admitted = admitted.to_vec();
sorted_admitted
.sort_by(|a, b| format!("{:?}", a.requirement).cmp(&format!("{:?}", b.requirement)));
let core = PlanFingerprint {
schema_version: BOUNDARY_PLAN_SCHEMA_VERSION,
backend,
snapshot,
admitted: &sorted_admitted,
workload: format!("{:?}", spec.workload),
budget_request: format!("{:?}", spec.budgets),
admitted_budgets: format!("{admitted_budgets:?}"),
evidence: format!("{:?}", spec.evidence),
};
let bytes = batpak::canonical::to_bytes(&core)?;
Ok(BoundaryPlanHash(batpak::event::hash::compute_hash(&bytes)))
}
fn budget_profile_digest(
snapshot: &BackendProfileSnapshot,
) -> Result<Digest32, rmp_serde::encode::Error> {
let bytes = batpak::canonical::to_bytes(&snapshot.budget)?;
Ok(batpak::event::hash::compute_hash(&bytes))
}
#[derive(serde::Serialize)]
struct PlanFingerprint<'a> {
schema_version: u16,
backend: BackendId,
snapshot: &'a BackendProfileSnapshot,
admitted: &'a [AdmittedRequirement],
workload: String,
budget_request: String,
admitted_budgets: String,
evidence: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum RunStep {
Observed(ObservedFact),
Sealed(Box<BoundaryReport>),
Faulted(String),
}
pub struct BoundaryRun {
facts: std::vec::IntoIter<ObservedFact>,
body: Option<BoundaryReportBody>,
backend: BackendId,
}
impl BoundaryRun {
pub fn drive_step(&mut self) -> Option<RunStep> {
if let Some(fact) = self.facts.next() {
return Some(RunStep::Observed(fact));
}
let body = self.body.take()?;
match body.body_hash() {
Ok(body_hash) => Some(RunStep::Sealed(Box::new(BoundaryReport {
body,
body_hash,
}))),
Err(error) => Some(RunStep::Faulted(format!(
"report sealing failed on {}: {error}",
self.backend
))),
}
}
}
impl Iterator for BoundaryRun {
type Item = RunStep;
fn next(&mut self) -> Option<RunStep> {
self.drive_step()
}
}
pub struct BoundaryRunner<'r> {
registry: &'r BackendRegistry,
}
impl<'r> BoundaryRunner<'r> {
#[must_use]
pub fn new(registry: &'r BackendRegistry) -> Self {
Self { registry }
}
pub fn begin(&self, plan: &BoundaryPlan) -> Result<BoundaryRun, PlanError> {
let backend =
self.registry
.backend(&plan.backend)
.ok_or_else(|| PlanError::UnknownBackend {
backend: plan.backend.clone(),
})?;
let body: BoundaryReportBody = backend.execute(plan);
Ok(BoundaryRun {
facts: body.observed.clone().into_iter(),
body: Some(body),
backend: plan.backend.clone(),
})
}
pub fn run(&self, plan: &BoundaryPlan) -> Result<BoundaryReport, PlanError> {
let mut run = self.begin(plan)?;
loop {
match run.drive_step() {
Some(RunStep::Observed(_)) => {}
Some(RunStep::Sealed(report)) => return Ok(*report),
Some(RunStep::Faulted(detail)) => {
return Err(PlanError::ProfileInsufficient {
backend: plan.backend.clone(),
detail,
})
}
None => {
return Err(PlanError::ProfileInsufficient {
backend: plan.backend.clone(),
detail: "run ended with no terminal step".to_string(),
})
}
}
}
}
}
#[cfg(test)]
mod planner_shadow_integration_tests {
use super::{derive_minimums, BackendRegistry, BoundaryPlanner};
use crate::contract::backend::Backend;
use crate::contract::budget::{
BudgetDimension, BudgetFailure, BudgetRequirements, DerivedMinimums,
};
use crate::contract::capability::{
Capability, FdPolicy, NetPolicy, SpawnPolicy, SupportVerdict,
};
use crate::contract::host_control::{HostControl, PathView, StdStreams};
use crate::contract::ids::BackendId;
use crate::contract::plan::{
BoundaryPlan, BoundaryRequirement, BoundarySpec, EvidenceRequirements, PlanError, Workload,
};
use crate::contract::report::BoundaryReportBody;
use crate::contract::support::{BackendProfile, BackendProfileSnapshot, SupportMatrix};
use crate::InertBackend;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
fn launch_control() -> HostControl {
HostControl::LaunchWorkload
}
fn admissible_spec() -> BoundarySpec {
BoundarySpec {
workload: Workload::Process {
exe: "/bin/true".to_string(),
args: vec![],
},
capabilities: vec![],
controls: vec![launch_control()],
budgets: BudgetRequirements::deny_all(),
evidence: EvidenceRequirements {
require_captured_streams: false,
require_exit_status: true,
},
}
}
fn registry_with(backend: Arc<dyn Backend>) -> BackendRegistry {
let mut registry = BackendRegistry::new();
registry.register(backend);
registry
}
fn inert_id() -> BackendId {
BackendId::new(InertBackend::ID)
}
#[test]
fn inert_deterministically_refuses_a_budgeted_spec_at_the_floor() {
let registry = registry_with(Arc::new(InertBackend::new()));
let planner = BoundaryPlanner::new(®istry);
let spec = admissible_spec();
let first = planner.plan(&spec, &inert_id());
let second = planner.plan(&spec, &inert_id());
assert_eq!(first, second, "Inert refuses deterministically");
assert!(
matches!(
first,
Err(PlanError::BudgetRefused {
dimension: BudgetDimension::Wall,
failure: BudgetFailure::BelowDerivedMinimum,
..
})
),
"the floor refuses at the budget membrane: {first:?}"
);
}
#[test]
fn refuses_unsupported_requirement_at_support_membrane() {
let registry = registry_with(Arc::new(InertBackend::new()));
let planner = BoundaryPlanner::new(®istry);
let mut spec = admissible_spec();
spec.capabilities = vec![Capability::Network {
policy: NetPolicy::DenyAll,
}];
let error = planner.plan(&spec, &inert_id()).expect_err("refuse");
assert_eq!(
error,
PlanError::Unsupported {
requirement: BoundaryRequirement::Capability(Capability::Network {
policy: NetPolicy::DenyAll,
}),
backend: inert_id(),
}
);
}
#[test]
fn refuses_unsatisfiable_evidence_at_evidence_membrane() {
let registry = registry_with(Arc::new(InertBackend::new()));
let planner = BoundaryPlanner::new(®istry);
let mut spec = admissible_spec();
spec.evidence.require_captured_streams = true;
let error = planner.plan(&spec, &inert_id()).expect_err("refuse");
assert!(
matches!(error, PlanError::EvidenceUnsatisfiable { .. }),
"expected evidence refusal, got {error:?}"
);
}
#[test]
fn unknown_backend_is_rejected() {
let registry = BackendRegistry::new();
let planner = BoundaryPlanner::new(®istry);
let error = planner
.plan(&admissible_spec(), &BackendId::new("ghost"))
.expect_err("unknown");
assert!(matches!(error, PlanError::UnknownBackend { .. }));
}
struct ProbeCounting {
inner: InertBackend,
probes: Arc<AtomicUsize>,
}
impl Backend for ProbeCounting {
fn id(&self) -> BackendId {
self.inner.id()
}
fn support(&self) -> &SupportMatrix {
self.inner.support()
}
fn probe(&self) -> BackendProfileSnapshot {
self.probes.fetch_add(1, Ordering::SeqCst);
self.inner.probe()
}
fn profile(&self, snap: &BackendProfileSnapshot) -> BackendProfile {
self.inner.profile(snap)
}
fn classify(&self, req: &BoundaryRequirement, profile: &BackendProfile) -> SupportVerdict {
self.inner.classify(req, profile)
}
fn execute(&self, plan: &BoundaryPlan) -> BoundaryReportBody {
self.inner.execute(plan)
}
}
#[test]
fn planning_probes_the_backend_exactly_once() {
let probes = Arc::new(AtomicUsize::new(0));
let backend = Arc::new(ProbeCounting {
inner: InertBackend::new(),
probes: Arc::clone(&probes),
});
let registry = registry_with(backend);
let planner = BoundaryPlanner::new(®istry);
let _ = planner.plan(&admissible_spec(), &inert_id());
assert_eq!(
probes.load(Ordering::SeqCst),
1,
"both admission paths must share one probe, not re-probe"
);
}
#[test]
fn derived_minimums_are_zero_without_a_launch() {
let mut spec = admissible_spec();
spec.controls = vec![];
assert_eq!(derive_minimums(&spec), DerivedMinimums::default());
}
#[test]
fn a_launch_implies_process_resource_and_std_handle_floors() {
let minimums = derive_minimums(&admissible_spec());
assert_eq!(minimums.process_count, 1);
assert_eq!(minimums.wall_micros, 1);
assert_eq!(minimums.cpu_micros, 1);
assert_eq!(minimums.resident_bytes, 1);
assert_eq!(minimums.handle_count, 3, "the three standard descriptors");
assert_eq!(minimums.storage_bytes, 0);
assert_eq!(minimums.network_bytes, 0);
}
#[test]
fn structural_floors_from_streams_fds_spawn_storage_and_network() {
let mut spec = admissible_spec();
spec.controls = vec![
HostControl::LaunchWorkload,
HostControl::CaptureStreams {
streams: StdStreams {
stdout: true,
stderr: true,
stdin: false,
},
},
HostControl::TempRoot {
visibility: PathView::PrivateToBoundary,
},
];
spec.capabilities = vec![
Capability::ChildSpawn {
policy: SpawnPolicy::AllowDescendantsWithinBoundary,
},
Capability::InheritedFds {
policy: FdPolicy::Only(vec![5, 6]),
},
Capability::Network {
policy: NetPolicy::AllowList(vec![]),
},
];
let minimums = derive_minimums(&spec);
assert_eq!(
minimums.process_count, 2,
"child-spawn needs >= 2 processes"
);
assert_eq!(
minimums.handle_count,
3 + 2 + 2,
"std(3) + captured stdout/stderr(2) + inherited fds(2)"
);
assert_eq!(minimums.storage_bytes, 1, "a temp root needs > 0 storage");
assert_eq!(
minimums.network_bytes, 1,
"requested network access cannot be a 0-byte budget"
);
}
}