use std::{error::Error, fmt};
use sim_kernel::{ContentId, Datum, NumberLiteral, Symbol};
use super::{
DeterministicImportManifest, ProjectorPolicy, ProjectorQualification, QualifiedRuntime,
QualifiedSourceClosure,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeSourceEvidence {
pub code: ContentId,
pub dependencies: ContentId,
pub review: ContentId,
pub source_and_dependencies_reviewed: bool,
pub ambient_io_closed: bool,
pub hidden_state_reviewed: bool,
pub loaded_code_matches: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClosedWasmEvidence {
pub module: ContentId,
pub imports: DeterministicImportManifest,
pub runtime: QualifiedRuntime,
pub admission: ContentId,
pub import_manifest_complete: bool,
pub start_behavior_checked: bool,
pub budgets_enforced: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum QualificationError {
NativeSourceReviewMissing,
NativeAmbientInput,
NativeCodeMismatch,
IncompleteImportManifest,
ImportManifestMismatch,
ForbiddenImport(String),
StartBehaviorUnchecked,
RuntimeSemanticsUnqualified,
RuntimeBudgetsUnenforced,
CanonicalPolicy(String),
}
impl fmt::Display for QualificationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{self:?}")
}
}
impl Error for QualificationError {}
#[derive(Clone, Copy, Debug, Default)]
pub struct ProjectorQualificationVerifier;
impl ProjectorQualificationVerifier {
pub fn trusted_native(
policy: &ProjectorPolicy,
evidence: NativeSourceEvidence,
) -> Result<ProjectorQualification, QualificationError> {
if !evidence.source_and_dependencies_reviewed {
return Err(QualificationError::NativeSourceReviewMissing);
}
if !evidence.ambient_io_closed || !evidence.hidden_state_reviewed {
return Err(QualificationError::NativeAmbientInput);
}
if !evidence.loaded_code_matches {
return Err(QualificationError::NativeCodeMismatch);
}
Ok(ProjectorQualification::TrustedNative {
source: QualifiedSourceClosure {
code: evidence.code,
dependencies: evidence.dependencies,
review: evidence.review,
},
policy: policy_id(policy)?,
})
}
pub fn closed_wasm(
policy: &ProjectorPolicy,
evidence: ClosedWasmEvidence,
) -> Result<ProjectorQualification, QualificationError> {
if !evidence.import_manifest_complete {
return Err(QualificationError::IncompleteImportManifest);
}
if evidence.imports != policy.imports {
return Err(QualificationError::ImportManifestMismatch);
}
for import in &evidence.imports.imports {
if is_forbidden_import(import) {
return Err(QualificationError::ForbiddenImport(import.clone()));
}
}
if !evidence.start_behavior_checked {
return Err(QualificationError::StartBehaviorUnchecked);
}
let semantics = &evidence.runtime.semantics;
if !semantics.canonical_nan || !semantics.canonical_collections || !semantics.fresh_instance
{
return Err(QualificationError::RuntimeSemanticsUnqualified);
}
if !evidence.budgets_enforced {
return Err(QualificationError::RuntimeBudgetsUnenforced);
}
Ok(ProjectorQualification::ClosedWasm {
module: evidence.module,
policy: policy_id(policy)?,
runtime: evidence.runtime,
imports: evidence.imports,
admission: evidence.admission,
})
}
}
pub(crate) fn policy_id(policy: &ProjectorPolicy) -> Result<ContentId, QualificationError> {
let input_facts = policy
.reads
.facts()
.map(|fact| Datum::String(fact.as_str().to_owned()))
.collect();
let imports = policy
.imports
.imports
.iter()
.cloned()
.map(Datum::String)
.collect();
let fields = vec![
(
Symbol::new("input-shape"),
content_id_datum(&policy.input_shape),
),
(Symbol::new("reads"), Datum::Vector(input_facts)),
(Symbol::new("imports"), Datum::Vector(imports)),
(
Symbol::new("execution"),
Datum::Node {
tag: Symbol::qualified("projection", "execution-semantics-v1"),
fields: vec![
(
Symbol::new("id"),
Datum::String(policy.execution.id.clone()),
),
(
Symbol::new("canonical-nan"),
Datum::Bool(policy.execution.canonical_nan),
),
(
Symbol::new("canonical-collections"),
Datum::Bool(policy.execution.canonical_collections),
),
(
Symbol::new("fresh-instance"),
Datum::Bool(policy.execution.fresh_instance),
),
],
},
),
(
Symbol::new("max-inputs"),
number_datum(policy.budgets.max_inputs as u64),
),
(
Symbol::new("max-output-bytes"),
number_datum(policy.budgets.max_output_bytes as u64),
),
(
Symbol::new("max-fuel"),
number_datum(policy.budgets.max_fuel),
),
(
Symbol::new("max-memory-bytes"),
number_datum(policy.budgets.max_memory_bytes as u64),
),
(
Symbol::new("requires-confinement"),
Datum::Bool(policy.requires_confinement),
),
];
Datum::Node {
tag: Symbol::qualified("projection", "projector-policy-v1"),
fields,
}
.content_id()
.map_err(|error| QualificationError::CanonicalPolicy(error.to_string()))
}
pub(crate) fn content_id_datum(id: &ContentId) -> Datum {
Datum::Node {
tag: Symbol::qualified("core", "content-id-v1"),
fields: vec![
(
Symbol::new("algorithm"),
Datum::Symbol(id.algorithm.clone()),
),
(Symbol::new("bytes"), Datum::Bytes(id.bytes.to_vec())),
],
}
}
fn number_datum(value: u64) -> Datum {
Datum::Number(NumberLiteral {
domain: Symbol::qualified("projection", "u64"),
canonical: value.to_string(),
})
}
fn is_forbidden_import(import: &str) -> bool {
const FORBIDDEN: &[&str] = &[
"wasi",
"filesystem",
"path_",
"proc",
"environment",
"environ",
"clock",
"time",
"random",
"network",
"socket",
"thread",
"shared-memory",
];
let lower = import.to_ascii_lowercase();
FORBIDDEN.iter().any(|needle| lower.contains(needle))
}