sim_incremental_core/projection/
admission.rs1use std::{error::Error, fmt};
2
3use sim_kernel::{ContentId, Datum, NumberLiteral, Symbol};
4
5use super::{
6 DeterministicImportManifest, ProjectorPolicy, ProjectorQualification, QualifiedRuntime,
7 QualifiedSourceClosure,
8};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct NativeSourceEvidence {
13 pub code: ContentId,
15 pub dependencies: ContentId,
17 pub review: ContentId,
19 pub source_and_dependencies_reviewed: bool,
21 pub ambient_io_closed: bool,
23 pub hidden_state_reviewed: bool,
25 pub loaded_code_matches: bool,
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct ClosedWasmEvidence {
32 pub module: ContentId,
34 pub imports: DeterministicImportManifest,
36 pub runtime: QualifiedRuntime,
38 pub admission: ContentId,
40 pub import_manifest_complete: bool,
42 pub start_behavior_checked: bool,
44 pub budgets_enforced: bool,
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
50pub enum QualificationError {
51 NativeSourceReviewMissing,
53 NativeAmbientInput,
55 NativeCodeMismatch,
57 IncompleteImportManifest,
59 ImportManifestMismatch,
61 ForbiddenImport(String),
63 StartBehaviorUnchecked,
65 RuntimeSemanticsUnqualified,
67 RuntimeBudgetsUnenforced,
69 CanonicalPolicy(String),
71}
72
73impl fmt::Display for QualificationError {
74 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(formatter, "{self:?}")
76 }
77}
78
79impl Error for QualificationError {}
80
81#[derive(Clone, Copy, Debug, Default)]
83pub struct ProjectorQualificationVerifier;
84
85impl ProjectorQualificationVerifier {
86 pub fn trusted_native(
88 policy: &ProjectorPolicy,
89 evidence: NativeSourceEvidence,
90 ) -> Result<ProjectorQualification, QualificationError> {
91 if !evidence.source_and_dependencies_reviewed {
92 return Err(QualificationError::NativeSourceReviewMissing);
93 }
94 if !evidence.ambient_io_closed || !evidence.hidden_state_reviewed {
95 return Err(QualificationError::NativeAmbientInput);
96 }
97 if !evidence.loaded_code_matches {
98 return Err(QualificationError::NativeCodeMismatch);
99 }
100 Ok(ProjectorQualification::TrustedNative {
101 source: QualifiedSourceClosure {
102 code: evidence.code,
103 dependencies: evidence.dependencies,
104 review: evidence.review,
105 },
106 policy: policy_id(policy)?,
107 })
108 }
109
110 pub fn closed_wasm(
112 policy: &ProjectorPolicy,
113 evidence: ClosedWasmEvidence,
114 ) -> Result<ProjectorQualification, QualificationError> {
115 if !evidence.import_manifest_complete {
116 return Err(QualificationError::IncompleteImportManifest);
117 }
118 if evidence.imports != policy.imports {
119 return Err(QualificationError::ImportManifestMismatch);
120 }
121 for import in &evidence.imports.imports {
122 if is_forbidden_import(import) {
123 return Err(QualificationError::ForbiddenImport(import.clone()));
124 }
125 }
126 if !evidence.start_behavior_checked {
127 return Err(QualificationError::StartBehaviorUnchecked);
128 }
129 let semantics = &evidence.runtime.semantics;
130 if !semantics.canonical_nan || !semantics.canonical_collections || !semantics.fresh_instance
131 {
132 return Err(QualificationError::RuntimeSemanticsUnqualified);
133 }
134 if !evidence.budgets_enforced {
135 return Err(QualificationError::RuntimeBudgetsUnenforced);
136 }
137 Ok(ProjectorQualification::ClosedWasm {
138 module: evidence.module,
139 policy: policy_id(policy)?,
140 runtime: evidence.runtime,
141 imports: evidence.imports,
142 admission: evidence.admission,
143 })
144 }
145}
146
147pub(crate) fn policy_id(policy: &ProjectorPolicy) -> Result<ContentId, QualificationError> {
148 let input_facts = policy
149 .reads
150 .facts()
151 .map(|fact| Datum::String(fact.as_str().to_owned()))
152 .collect();
153 let imports = policy
154 .imports
155 .imports
156 .iter()
157 .cloned()
158 .map(Datum::String)
159 .collect();
160 let fields = vec![
161 (
162 Symbol::new("input-shape"),
163 content_id_datum(&policy.input_shape),
164 ),
165 (Symbol::new("reads"), Datum::Vector(input_facts)),
166 (Symbol::new("imports"), Datum::Vector(imports)),
167 (
168 Symbol::new("execution"),
169 Datum::Node {
170 tag: Symbol::qualified("projection", "execution-semantics-v1"),
171 fields: vec![
172 (
173 Symbol::new("id"),
174 Datum::String(policy.execution.id.clone()),
175 ),
176 (
177 Symbol::new("canonical-nan"),
178 Datum::Bool(policy.execution.canonical_nan),
179 ),
180 (
181 Symbol::new("canonical-collections"),
182 Datum::Bool(policy.execution.canonical_collections),
183 ),
184 (
185 Symbol::new("fresh-instance"),
186 Datum::Bool(policy.execution.fresh_instance),
187 ),
188 ],
189 },
190 ),
191 (
192 Symbol::new("max-inputs"),
193 number_datum(policy.budgets.max_inputs as u64),
194 ),
195 (
196 Symbol::new("max-output-bytes"),
197 number_datum(policy.budgets.max_output_bytes as u64),
198 ),
199 (
200 Symbol::new("max-fuel"),
201 number_datum(policy.budgets.max_fuel),
202 ),
203 (
204 Symbol::new("max-memory-bytes"),
205 number_datum(policy.budgets.max_memory_bytes as u64),
206 ),
207 (
208 Symbol::new("requires-confinement"),
209 Datum::Bool(policy.requires_confinement),
210 ),
211 ];
212 Datum::Node {
213 tag: Symbol::qualified("projection", "projector-policy-v1"),
214 fields,
215 }
216 .content_id()
217 .map_err(|error| QualificationError::CanonicalPolicy(error.to_string()))
218}
219
220pub(crate) fn content_id_datum(id: &ContentId) -> Datum {
221 Datum::Node {
222 tag: Symbol::qualified("core", "content-id-v1"),
223 fields: vec![
224 (
225 Symbol::new("algorithm"),
226 Datum::Symbol(id.algorithm.clone()),
227 ),
228 (Symbol::new("bytes"), Datum::Bytes(id.bytes.to_vec())),
229 ],
230 }
231}
232
233fn number_datum(value: u64) -> Datum {
234 Datum::Number(NumberLiteral {
235 domain: Symbol::qualified("projection", "u64"),
236 canonical: value.to_string(),
237 })
238}
239
240fn is_forbidden_import(import: &str) -> bool {
241 const FORBIDDEN: &[&str] = &[
242 "wasi",
243 "filesystem",
244 "path_",
245 "proc",
246 "environment",
247 "environ",
248 "clock",
249 "time",
250 "random",
251 "network",
252 "socket",
253 "thread",
254 "shared-memory",
255 ];
256 let lower = import.to_ascii_lowercase();
257 FORBIDDEN.iter().any(|needle| lower.contains(needle))
258}