sim_incremental_core/projection/
engine.rs1use std::{
2 collections::{BTreeMap, BTreeSet},
3 sync::Arc,
4};
5
6use sim_kernel::{ContentId, Datum, Symbol};
7
8use super::{
9 ConfinementEvidence, FederatedClosure, MediatedAccessWitness, ProjectionDigest,
10 ProjectionError, ProjectionKindRef, ProjectionProvider, ProjectionResult, ProjectionSpec,
11 ProjectorPolicy, ProjectorQualification, admission::content_id_datum, admission::policy_id,
12};
13
14pub trait ConfigShapeVerifier {
16 fn verify(&self, shape: &ContentId, config: &Datum) -> Result<(), String>;
18}
19
20#[derive(Default)]
22pub struct ProjectionRegistry {
23 providers: BTreeMap<ProjectionKindRef, RegisteredProvider>,
24}
25
26struct RegisteredProvider {
27 identity: super::PackageIdentity,
28 provider: Arc<dyn ProjectionProvider>,
29}
30
31impl ProjectionRegistry {
32 #[must_use]
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn register(
40 &mut self,
41 identity: super::PackageIdentity,
42 provider: Arc<dyn ProjectionProvider>,
43 ) -> Result<(), ProjectionError> {
44 let kind = provider.kind().clone();
45 if self
46 .providers
47 .insert(kind.clone(), RegisteredProvider { identity, provider })
48 .is_some()
49 {
50 return Err(ProjectionError::DuplicateProvider(kind));
51 }
52 Ok(())
53 }
54
55 #[must_use]
57 pub fn get(
58 &self,
59 kind: &ProjectionKindRef,
60 ) -> Option<(&super::PackageIdentity, &Arc<dyn ProjectionProvider>)> {
61 self.providers
62 .get(kind)
63 .map(|registered| (®istered.identity, ®istered.provider))
64 }
65
66 pub fn kinds(&self) -> impl ExactSizeIterator<Item = &ProjectionKindRef> {
68 self.providers.keys()
69 }
70}
71
72pub struct ProjectionEngine<'a> {
74 registry: &'a ProjectionRegistry,
75 shapes: &'a dyn ConfigShapeVerifier,
76 closure: &'a FederatedClosure,
77}
78
79impl<'a> ProjectionEngine<'a> {
80 #[must_use]
82 pub fn new(
83 registry: &'a ProjectionRegistry,
84 shapes: &'a dyn ConfigShapeVerifier,
85 closure: &'a FederatedClosure,
86 ) -> Self {
87 Self {
88 registry,
89 shapes,
90 closure,
91 }
92 }
93
94 pub fn project(
96 &self,
97 world: &super::ObservedWorld,
98 spec: &ProjectionSpec,
99 policy: &ProjectorPolicy,
100 qualification: Option<&ProjectorQualification>,
101 confinement: Option<ConfinementEvidence>,
102 ) -> Result<ProjectionResult, ProjectionError> {
103 let qualification = qualification.ok_or_else(|| {
104 ProjectionError::UnqualifiedProjector("projector qualification is missing".to_owned())
105 })?;
106 let (loaded_identity, provider) = self
107 .registry
108 .get(&spec.kind)
109 .ok_or_else(|| ProjectionError::UnknownProvider(spec.kind.clone()))?;
110 if loaded_identity != &spec.provider {
111 return Err(ProjectionError::CodeIdentityMismatch);
112 }
113 if provider.config_shape() != &spec.config_shape {
114 return Err(ProjectionError::ConfigShapeMismatch);
115 }
116 self.shapes
117 .verify(&spec.config_shape, &spec.config)
118 .map_err(ProjectionError::InvalidConfig)?;
119 let expected_policy = policy_id(policy)
120 .map_err(|error| ProjectionError::UnqualifiedProjector(error.to_string()))?;
121 if qualification.policy() != &expected_policy {
122 return Err(ProjectionError::UnqualifiedProjector(
123 "qualification policy identity differs".to_owned(),
124 ));
125 }
126 if qualification.implementation() != &spec.provider.code {
127 return Err(ProjectionError::CodeIdentityMismatch);
128 }
129 if policy.requires_confinement {
130 let evidence = confinement.as_ref().ok_or_else(|| {
131 ProjectionError::UnavailableConfinement("required membrane absent".to_owned())
132 })?;
133 if !evidence.live {
134 return Err(ProjectionError::UnavailableConfinement(
135 "required membrane unavailable on selected host".to_owned(),
136 ));
137 }
138 }
139 if policy.reads.facts().len() > policy.budgets.max_inputs {
140 return Err(ProjectionError::BudgetExceeded("selected inputs"));
141 }
142 let inputs = world.select(&policy.reads)?;
143 let output = provider.project(&inputs, &spec.config)?;
144 let accessed = inputs.accessed();
145 for fact in &accessed {
146 if !output.dependencies.contains(fact) {
147 return Err(ProjectionError::UndeclaredAccess {
148 accessed: fact.clone(),
149 });
150 }
151 }
152 for fact in &output.dependencies {
153 if !accessed.contains(fact) {
154 return Err(ProjectionError::UnreadDependency(fact.clone()));
155 }
156 }
157 let output_bytes = output
158 .value
159 .canonical_bytes()
160 .map_err(|error| ProjectionError::Canonical(error.to_string()))?;
161 if output_bytes.len() > policy.budgets.max_output_bytes {
162 return Err(ProjectionError::BudgetExceeded("projection output"));
163 }
164 let digest = projection_digest(
165 spec,
166 qualification,
167 &inputs,
168 &output.dependencies,
169 &output.value,
170 )?;
171 let affected = self.closure.affected(output.dependencies.iter().cloned());
172 let mut explanations = Vec::new();
173 for conclusion in &affected {
174 for fact in &output.dependencies {
175 if let Ok(explanation) = self.closure.explain(conclusion, fact) {
176 explanations.push(explanation);
177 }
178 }
179 }
180 Ok(ProjectionResult {
181 projection: output.value,
182 mediated_access: MediatedAccessWitness {
183 selected: policy.reads.facts().cloned().collect(),
184 accessed,
185 },
186 projector_qualification: qualification.clone(),
187 confinement,
188 digest,
189 affected,
190 explanations,
191 })
192 }
193}
194
195fn projection_digest(
196 spec: &ProjectionSpec,
197 qualification: &ProjectorQualification,
198 inputs: &super::ProjectionInputs,
199 dependencies: &BTreeSet<super::FactId>,
200 output: &Datum,
201) -> Result<ProjectionDigest, ProjectionError> {
202 let facts = dependencies
203 .iter()
204 .map(|id| {
205 let value = inputs
206 .get(id)
207 .expect("provider dependency was validated as an accessed selected fact");
208 Datum::Node {
209 tag: Symbol::qualified("projection", "semantic-input-v1"),
210 fields: vec![
211 (Symbol::new("id"), Datum::String(id.as_str().to_owned())),
212 (Symbol::new("value"), value.clone()),
213 ],
214 }
215 })
216 .collect();
217 let preimage = Datum::Node {
218 tag: Symbol::qualified("projection", "semantic-projection-digest-v1"),
219 fields: vec![
220 (
221 Symbol::new("kind"),
222 Datum::String(spec.kind.as_str().to_owned()),
223 ),
224 (
225 Symbol::new("package"),
226 Datum::String(spec.provider.name.clone()),
227 ),
228 (
229 Symbol::new("version"),
230 Datum::String(spec.provider.version.clone()),
231 ),
232 (
233 Symbol::new("implementation"),
234 content_id_datum(qualification.implementation()),
235 ),
236 (
237 Symbol::new("policy"),
238 content_id_datum(qualification.policy()),
239 ),
240 (Symbol::new("config"), spec.config.clone()),
241 (Symbol::new("inputs"), Datum::Vector(facts)),
242 (Symbol::new("projection"), output.clone()),
243 ],
244 };
245 preimage
246 .content_id()
247 .map(ProjectionDigest)
248 .map_err(|error| ProjectionError::Canonical(error.to_string()))
249}