Skip to main content

greentic_component_runtime/
loader.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use component_manifest::{CapabilityRef, CompiledExportSchema, ComponentInfo, WitCompat};
5use greentic_interfaces_host::component::v0_6::exports::greentic::component::node::{
6    ComponentDescriptor, GuestIndices,
7};
8use greentic_types::cbor::canonical;
9use greentic_types::schemas::component::v0_6_0::ComponentDescribe;
10use jsonschema::{Validator, validator_for};
11use serde_json::{Map, Value, json};
12use wasmtime::component::{Component as WasmComponent, Func, InstancePre, Val};
13use wasmtime::{Config, Engine};
14
15use crate::error::CompError;
16use crate::host_imports::{HostState, build_linker};
17use crate::policy::LoadPolicy;
18
19const SELF_DESCRIBE_TAG: [u8; 3] = [0xd9, 0xd9, 0xf7];
20
21#[derive(Debug, Clone)]
22pub struct ComponentRef {
23    pub name: String,
24    pub locator: String,
25}
26
27pub struct Loader;
28
29impl Default for Loader {
30    fn default() -> Self {
31        Self
32    }
33}
34
35impl Loader {
36    pub fn load(
37        &self,
38        cref: &ComponentRef,
39        policy: &LoadPolicy,
40    ) -> Result<ComponentHandle, CompError> {
41        let artifact = policy
42            .store
43            .fetch_from_str(&cref.locator, &policy.verification)?;
44
45        let engine = create_engine()?;
46        let component = WasmComponent::from_binary(&engine, &artifact.bytes)?;
47
48        let linker = build_linker(&engine, &policy.host)?;
49        let instance_pre = linker.instantiate_pre(&component)?;
50        let guest_indices = GuestIndices::new(&instance_pre)?;
51        let host_state = HostState::empty(policy.host.clone());
52        let mut store = wasmtime::Store::new(&engine, host_state);
53
54        let instance = instance_pre.instantiate(&mut store)?;
55        let guest = guest_indices.load(&mut store, &instance)?;
56        let descriptor = guest.call_describe(&mut store)?;
57        let config_schema_value =
58            load_config_schema_from_describe(&instance, &mut store)?.unwrap_or_else(|| json!({}));
59        let info = component_info_from_descriptor(&descriptor, config_schema_value.clone());
60        let config_schema = validator_for(&config_schema_value)
61            .map_err(|err| CompError::SchemaValidation(err.to_string()))?;
62
63        Ok(ComponentHandle {
64            inner: Arc::new(ComponentInner {
65                cref: cref.clone(),
66                info,
67                config_schema: Arc::new(config_schema),
68                engine,
69                instance_pre,
70                guest_indices,
71                host_policy: policy.host.clone(),
72                bindings: Mutex::new(HashMap::new()),
73            }),
74        })
75    }
76
77    pub fn describe(&self, handle: &ComponentHandle) -> Result<ComponentInfo, CompError> {
78        Ok(handle.inner.info.clone())
79    }
80}
81
82fn component_info_from_descriptor(
83    descriptor: &ComponentDescriptor,
84    config_schema: Value,
85) -> ComponentInfo {
86    let capabilities = descriptor
87        .capabilities
88        .iter()
89        .cloned()
90        .map(CapabilityRef)
91        .collect();
92    let exports = descriptor
93        .ops
94        .iter()
95        .map(|op| CompiledExportSchema {
96            operation: op.name.clone(),
97            description: op.summary.clone(),
98            input_schema: None,
99            output_schema: None,
100        })
101        .collect();
102
103    let raw = json!({
104        "name": descriptor.name,
105        "description": descriptor.summary,
106        "capabilities": descriptor.capabilities,
107        "exports": descriptor.ops.iter().map(|op| json!({ "operation": op.name, "description": op.summary })).collect::<Vec<_>>(),
108        "config_schema": config_schema,
109        "secret_requirements": [],
110        "wit_compat": {
111            "package": "greentic:component",
112            "min": "0.6.0"
113        }
114    });
115
116    ComponentInfo {
117        name: Some(descriptor.name.clone()),
118        description: descriptor.summary.clone(),
119        capabilities,
120        exports,
121        config_schema,
122        secret_requirements: Vec::new(),
123        wit_compat: WitCompat {
124            package: "greentic:component".to_string(),
125            min: "0.6.0".to_string(),
126            max: None,
127        },
128        metadata: Map::new(),
129        raw,
130    }
131}
132
133fn load_config_schema_from_describe(
134    instance: &wasmtime::component::Instance,
135    store: &mut wasmtime::Store<HostState>,
136) -> Result<Option<Value>, CompError> {
137    let Some(interface_index) = resolve_interface_index(instance, store, "component-descriptor")
138    else {
139        return Ok(None);
140    };
141    let Some(func_index) =
142        instance.get_export_index(&mut *store, Some(&interface_index), "describe")
143    else {
144        return Ok(None);
145    };
146    let func = instance.get_func(&mut *store, func_index).ok_or_else(|| {
147        CompError::Runtime("component-descriptor.describe is not callable".into())
148    })?;
149    let describe_bytes = call_component_func(store, &func, &[]).and_then(|values| {
150        values
151            .first()
152            .ok_or_else(|| CompError::Runtime("describe returned no values".into()))
153            .and_then(val_to_bytes)
154    })?;
155    let payload = strip_self_describe_tag(&describe_bytes);
156    let describe: ComponentDescribe = canonical::from_cbor(payload)
157        .map_err(|err| CompError::SchemaValidation(err.to_string()))?;
158    serde_json::to_value(describe.config_schema)
159        .map(Some)
160        .map_err(CompError::from)
161}
162
163fn resolve_interface_index(
164    instance: &wasmtime::component::Instance,
165    store: &mut wasmtime::Store<HostState>,
166    interface: &str,
167) -> Option<wasmtime::component::ComponentExportIndex> {
168    for candidate in interface_candidates(interface) {
169        if let Some(index) = instance.get_export_index(&mut *store, None, &candidate) {
170            return Some(index);
171        }
172    }
173    None
174}
175
176fn interface_candidates(interface: &str) -> [String; 3] {
177    [
178        interface.to_string(),
179        format!("greentic:component/{interface}@0.6.0"),
180        format!("greentic:component/{interface}"),
181    ]
182}
183
184fn call_component_func(
185    store: &mut wasmtime::Store<HostState>,
186    func: &Func,
187    params: &[Val],
188) -> Result<Vec<Val>, CompError> {
189    let results_len = func.ty(&mut *store).results().len();
190    let mut results = vec![Val::Bool(false); results_len];
191    func.call(&mut *store, params, &mut results)
192        .map_err(|err| CompError::Runtime(format!("call failed: {err}")))?;
193    Ok(results)
194}
195
196fn val_to_bytes(val: &Val) -> Result<Vec<u8>, CompError> {
197    match val {
198        Val::List(items) => {
199            let mut out = Vec::with_capacity(items.len());
200            for item in items {
201                match item {
202                    Val::U8(byte) => out.push(*byte),
203                    _ => {
204                        return Err(CompError::Runtime(
205                            "describe returned list with non-u8 items".to_string(),
206                        ));
207                    }
208                }
209            }
210            Ok(out)
211        }
212        _ => Err(CompError::Runtime(
213            "describe returned non-byte list payload".to_string(),
214        )),
215    }
216}
217
218fn strip_self_describe_tag(bytes: &[u8]) -> &[u8] {
219    if bytes.starts_with(&SELF_DESCRIBE_TAG) {
220        &bytes[SELF_DESCRIBE_TAG.len()..]
221    } else {
222        bytes
223    }
224}
225
226fn create_engine() -> Result<Engine, CompError> {
227    let mut config = Config::new();
228    config.wasm_component_model(true);
229    config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
230    Engine::new(&config).map_err(|err| CompError::Runtime(err.to_string()))
231}
232
233pub struct ComponentHandle {
234    pub(crate) inner: Arc<ComponentInner>,
235}
236
237pub(crate) struct ComponentInner {
238    pub(crate) cref: ComponentRef,
239    pub(crate) info: ComponentInfo,
240    pub(crate) config_schema: Arc<Validator>,
241    pub(crate) engine: Engine,
242    pub(crate) instance_pre: InstancePre<HostState>,
243    pub(crate) guest_indices: GuestIndices,
244    pub(crate) host_policy: crate::policy::HostPolicy,
245    pub(crate) bindings: Mutex<HashMap<String, TenantBinding>>,
246}
247
248#[derive(Debug, Clone)]
249pub(crate) struct TenantBinding {
250    pub config: Value,
251    pub secrets: HashMap<String, Vec<u8>>,
252}
253
254impl ComponentHandle {
255    pub fn info(&self) -> &ComponentInfo {
256        &self.inner.info
257    }
258
259    pub fn cref(&self) -> &ComponentRef {
260        &self.inner.cref
261    }
262}
263
264impl Clone for ComponentHandle {
265    fn clone(&self) -> Self {
266        Self {
267            inner: Arc::clone(&self.inner),
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use serde_json::json;
276
277    fn descriptor_fixture() -> ComponentDescriptor {
278        ComponentDescriptor {
279            name: "fixture".to_string(),
280            version: "0.1.0".to_string(),
281            summary: Some("summary".to_string()),
282            capabilities: vec!["telemetry".to_string()],
283            ops: vec![],
284            schemas: vec![],
285            setup: None,
286        }
287    }
288
289    #[test]
290    fn descriptor_maps_to_component_info() {
291        let config_schema = json!({"type":"object"});
292        let info = component_info_from_descriptor(&descriptor_fixture(), config_schema.clone());
293        assert_eq!(info.wit_compat.package, "greentic:component");
294        assert_eq!(info.wit_compat.min, "0.6.0");
295        assert_eq!(info.config_schema, config_schema);
296        assert_eq!(info.capabilities.len(), 1);
297    }
298
299    #[test]
300    fn strips_self_describe_tag_only_when_present() {
301        let tagged = [SELF_DESCRIBE_TAG.as_slice(), &[1_u8, 2, 3]].concat();
302        assert_eq!(strip_self_describe_tag(&tagged), &[1_u8, 2, 3]);
303        assert_eq!(strip_self_describe_tag(&[7_u8, 8, 9]), &[7_u8, 8, 9]);
304    }
305}