Skip to main content

hara_native/
package_wasm_loader.rs

1//! Trusted native-host loader for resolver-selected prebuilt Wasm package variants.
2//!
3//! This module deliberately consumes only verified package metadata and package-root bytes. It does
4//! not compile source code, discover ambient host services, or grant capabilities not admitted by
5//! the package resolver.
6
7#![cfg(not(target_arch = "wasm32"))]
8
9use std::fs;
10use std::path::Path;
11
12use crate::extension::{ExtensionManifest, WasmAbi, WasmExtension};
13use crate::package_manifest::{
14    PackageArtifactType, PackageManifest, PackageRuntimeRequirements, PackageSelection,
15};
16use crate::wasmtime_provider::WasmtimeExtensionProvider;
17
18pub struct LoadedPackageWasm {
19    pub identity: String,
20    pub entry_point: String,
21    pub extension: WasmExtension,
22}
23
24pub fn load_wasm_package(
25    manifest: &PackageManifest,
26    package_root: &Path,
27    requirements: &PackageRuntimeRequirements,
28    extension_manifest_source: &str,
29) -> Result<LoadedPackageWasm, String> {
30    let module = manifest
31        .wasm_imports
32        .keys()
33        .next()
34        .cloned()
35        .ok_or_else(|| {
36            "package/missing-wasm-import: package declares no Wasm imports".to_owned()
37        })?;
38    load_wasm_import_package(
39        manifest,
40        package_root,
41        &module,
42        requirements,
43        extension_manifest_source,
44    )
45}
46
47pub fn load_wasm_import_package(
48    manifest: &PackageManifest,
49    package_root: &Path,
50    module: &str,
51    requirements: &PackageRuntimeRequirements,
52    extension_manifest_source: &str,
53) -> Result<LoadedPackageWasm, String> {
54    manifest
55        .verify_files_at(package_root)
56        .map_err(|error| error.to_string())?;
57    let selection = manifest
58        .select_wasm_import(module, requirements)
59        .map_err(|error| error.to_string())?;
60    let PackageSelection::Variant(variant) = &selection else {
61        return Err("package/missing-artifact: portable package has no Wasm artifact".into());
62    };
63    if variant.artifact.artifact_type != PackageArtifactType::Wasm {
64        return Err(format!(
65            "package/artifact-type-mismatch: expected :wasm, got :{}",
66            variant.artifact.artifact_type.keyword()
67        ));
68    }
69    if variant.artifact.abi != "core.v1" && variant.artifact.abi != "memory.v1" {
70        return Err(format!(
71            "package/abi-mismatch: direct Wasm loader does not support {}",
72            variant.artifact.abi
73        ));
74    }
75
76    let artifact_path = package_root.join(&variant.artifact.path);
77    let bytes = fs::read(&artifact_path).map_err(|error| {
78        format!(
79            "package/missing-artifact: cannot read {}: {error}",
80            variant.artifact.path.display()
81        )
82    })?;
83    manifest
84        .verify_artifact_bytes(&selection, &bytes)
85        .map_err(|error| error.to_string())?;
86
87    let extension_manifest = ExtensionManifest::parse(extension_manifest_source, "package")?;
88    if extension_manifest.identity.as_deref() != Some(manifest.identity.as_str()) {
89        return Err("package/identity-mismatch: extension identity differs from package".into());
90    }
91    let expected_abi = match variant.artifact.abi.as_str() {
92        "core.v1" => WasmAbi::CoreV1,
93        "memory.v1" => WasmAbi::MemoryV1,
94        _ => unreachable!(),
95    };
96    if extension_manifest.abi != expected_abi {
97        return Err(
98            "package/abi-mismatch: extension manifest differs from selected variant".into(),
99        );
100    }
101    if extension_manifest.provider != "wasm" {
102        return Err(
103            "package/provider-mismatch: direct Wasm artifact requires :provider :wasm".into(),
104        );
105    }
106    if !variant.required_capabilities.iter().all(|capability| {
107        extension_manifest
108            .capabilities
109            .iter()
110            .any(|declared| declared == capability)
111    }) {
112        return Err(
113            "package/manifest-mismatch: required capabilities differ from extension".into(),
114        );
115    }
116    if !variant.host_calls.is_empty() || !extension_manifest.host_calls.is_empty() {
117        return Err(
118            "package/host-call-denied: direct Wasm packages cannot request host calls".into(),
119        );
120    }
121    if !variant.exports.iter().all(|export| {
122        extension_manifest
123            .exports
124            .iter()
125            .any(|(declared, _)| declared == export)
126    }) {
127        return Err(
128            "package/manifest-mismatch: selected exports are not declared by extension".into(),
129        );
130    }
131    if !extension_manifest.exports.iter().any(|(name, spec)| {
132        name == &variant.artifact.entry_point || spec.raw_name(name) == variant.artifact.entry_point
133    }) {
134        return Err("package/entry-point-mismatch: selected entry point is not exported".into());
135    }
136
137    let provider = match expected_abi {
138        WasmAbi::CoreV1 => WasmtimeExtensionProvider::compile(&bytes)?,
139        WasmAbi::MemoryV1 => {
140            return Err(
141                "package/binding-plan-required: memory.v1 package loading requires verified bindings.edn"
142                    .into(),
143            )
144        }
145        WasmAbi::HtaV1 => unreachable!(),
146    };
147    let extension = WasmExtension::new(extension_manifest, provider)?;
148    Ok(LoadedPackageWasm {
149        identity: manifest.identity.clone(),
150        entry_point: variant.artifact.entry_point.clone(),
151        extension,
152    })
153}