greentic_component_runtime/
loader.rs1use 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}