Skip to main content

greentic_ext_runtime/
runtime_roles.rs

1//! `roles` interface dispatch for design extensions.
2//!
3//! Mirrors the export-walking pattern in [`crate::runtime`] (see
4//! `render_bundle` / `validate_content`) but lives in its own module to
5//! keep `runtime.rs` under the workspace 500-line cap.
6
7use crate::error::RuntimeError;
8use crate::loaded::ExtensionId;
9use crate::runtime::ExtensionRuntime;
10use crate::types::{
11    CompileContext, Diagnostic, HostExtensionError, RoleError, RoleSpec, Severity, TargetKind,
12};
13
14/// Base interface name without version suffix.
15const IFACE_BASE: &str = "greentic:extension-design/roles";
16/// Version resolution order — newest first.
17const ROLES_VERSIONS: &[&str] = &["0.3.0", "0.2.0"];
18
19impl ExtensionRuntime {
20    /// List all roles exposed by a loaded design extension.
21    ///
22    /// Calls `greentic:extension-design/roles::list-roles` (resolved against
23    /// `@0.3.0` first, then `@0.2.0`).
24    /// Returns an empty vec when the extension does not export the
25    /// `roles` interface (older 0.1.0 extensions, for example) so
26    /// callers can treat it as "no roles published" without reaching
27    /// for `RuntimeError::Wasmtime`.
28    pub fn list_roles(&self, ext_id: &str) -> Result<Vec<RoleSpec>, RuntimeError> {
29        use crate::host_bindings::exports::greentic::extension_design0_2_0::roles::RoleSpec as WitRoleSpec;
30
31        let loaded = self
32            .loaded()
33            .get(&ExtensionId(ext_id.to_string()))
34            .cloned()
35            .ok_or_else(|| RuntimeError::NotFound(ext_id.to_string()))?;
36
37        let (mut store, instance) = loaded
38            .build_store_and_instance(
39                self.engine(),
40                self.host_overrides().clone(),
41                &crate::host_ports::HostCallContext::default(),
42            )
43            .map_err(RuntimeError::Wasmtime)?;
44
45        // Try newest first; fall back gracefully to empty if neither version
46        // is exported (older extensions that pre-date roles entirely).
47        let iface_idx = {
48            let mut found = None;
49            for &v in ROLES_VERSIONS {
50                let name = format!("{IFACE_BASE}@{v}");
51                if let Some(idx) = instance.get_export_index(&mut store, None, &name) {
52                    found = Some(idx);
53                    break;
54                }
55            }
56            match found {
57                Some(idx) => idx,
58                None => return Ok(Vec::new()),
59            }
60        };
61
62        let func_idx = instance
63            .get_export_index(&mut store, Some(&iface_idx), "list-roles")
64            .ok_or_else(|| {
65                RuntimeError::Wasmtime(anyhow::anyhow!(
66                    "interface '{IFACE_BASE}' does not export 'list-roles'"
67                ))
68            })?;
69
70        let func = instance
71            .get_typed_func::<(), (Vec<WitRoleSpec>,)>(&mut store, &func_idx)
72            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
73
74        let (roles,) = func
75            .call(&mut store, ())
76            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
77
78        Ok(roles.into_iter().map(wit_role_spec_to_host).collect())
79    }
80
81    /// Run the cheap-path validator for a role's DSL entry.
82    ///
83    /// Calls `greentic:extension-design/roles::validate-role` (resolved
84    /// against `@0.3.0` first, then `@0.2.0`).
85    /// Returns the diagnostic list verbatim (empty = valid, mirrors the
86    /// WIT contract). `RuntimeError` is reserved for host failures —
87    /// missing extension, missing interface, wasmtime trap.
88    pub fn validate_role(
89        &self,
90        ext_id: &str,
91        name: &str,
92        entry_json: &str,
93    ) -> Result<Vec<Diagnostic>, RuntimeError> {
94        use crate::host_bindings::exports::greentic::extension_design0_2_0::roles::Diagnostic as WitDiagnostic;
95
96        let loaded = self
97            .loaded()
98            .get(&ExtensionId(ext_id.to_string()))
99            .cloned()
100            .ok_or_else(|| RuntimeError::NotFound(ext_id.to_string()))?;
101
102        let (mut store, instance) = loaded
103            .build_store_and_instance(
104                self.engine(),
105                self.host_overrides().clone(),
106                &crate::host_ports::HostCallContext::default(),
107            )
108            .map_err(RuntimeError::Wasmtime)?;
109
110        let (iface_idx, iface_name, _version) = crate::runtime::resolve_iface_versions(
111            &mut store,
112            &instance,
113            IFACE_BASE,
114            ROLES_VERSIONS,
115        )?;
116
117        let func_idx = instance
118            .get_export_index(&mut store, Some(&iface_idx), "validate-role")
119            .ok_or_else(|| {
120                RuntimeError::Wasmtime(anyhow::anyhow!(
121                    "interface '{iface_name}' does not export 'validate-role'"
122                ))
123            })?;
124
125        let func = instance
126            .get_typed_func::<(String, String), (Vec<WitDiagnostic>,)>(&mut store, &func_idx)
127            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
128
129        let (diags,) = func
130            .call(&mut store, (name.to_string(), entry_json.to_string()))
131            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
132
133        Ok(diags.into_iter().map(wit_diagnostic_to_host).collect())
134    }
135
136    /// Compile a single DSL entry to its target representation.
137    ///
138    /// Calls `greentic:extension-design/roles::compile-role` (resolved against
139    /// `@0.3.0` first, then `@0.2.0`). A missing extension surfaces as
140    /// `RoleError::UnknownRole(ext_id)` (the registry can't tell "no
141    /// extension" apart from "no role" from the LLM's perspective and both
142    /// should retry with a hint). Host failures (wasmtime trap, missing
143    /// interface) surface as `RoleError::Host(HostExtensionError::Internal(_))`
144    /// so the caller can match exhaustively without juggling two error types.
145    pub fn compile_role(
146        &self,
147        ext_id: &str,
148        name: &str,
149        target: TargetKind,
150        entry_json: &str,
151        ctx: Option<&CompileContext>,
152    ) -> Result<String, RoleError> {
153        let loaded = self
154            .loaded()
155            .get(&ExtensionId(ext_id.to_string()))
156            .cloned()
157            .ok_or_else(|| RoleError::UnknownRole(ext_id.to_string()))?;
158
159        let (mut store, instance) = loaded
160            .build_store_and_instance(
161                self.engine(),
162                self.host_overrides().clone(),
163                &crate::host_ports::HostCallContext::default(),
164            )
165            .map_err(|e| {
166                RoleError::Host(HostExtensionError::Internal(format!(
167                    "instantiate '{ext_id}': {e}"
168                )))
169            })?;
170
171        let (iface_idx, iface_name, version) = crate::runtime::resolve_iface_versions(
172            &mut store,
173            &instance,
174            IFACE_BASE,
175            ROLES_VERSIONS,
176        )
177        .map_err(|e| {
178            RoleError::Host(HostExtensionError::Internal(format!(
179                "extension '{ext_id}' does not export '{IFACE_BASE}': {e}"
180            )))
181        })?;
182
183        let func_idx = instance
184            .get_export_index(&mut store, Some(&iface_idx), "compile-role")
185            .ok_or_else(|| {
186                RoleError::Host(HostExtensionError::Internal(format!(
187                    "interface '{iface_name}' does not export 'compile-role'"
188                )))
189            })?;
190
191        if version == "0.3.0" {
192            use crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::{
193                CompileContext as WitCompileContext, RoleError as WitRoleError,
194                TargetKind as WitTargetKind,
195            };
196
197            let func = instance
198                .get_typed_func::<
199                    (String, WitTargetKind, String, Option<WitCompileContext>),
200                    (Result<String, WitRoleError>,),
201                >(&mut store, &func_idx)
202                .map_err(|e| RoleError::Host(HostExtensionError::Internal(e.to_string())))?;
203
204            let wit_ctx = ctx.cloned().map(|c| WitCompileContext {
205                flow_entries_json: c.flow_entries_json,
206                flow_id: c.flow_id,
207                locale: c.locale,
208            });
209
210            let (result,) = func
211                .call(
212                    &mut store,
213                    (
214                        name.to_string(),
215                        target_to_wit_v03(target),
216                        entry_json.to_string(),
217                        wit_ctx,
218                    ),
219                )
220                .map_err(|e| RoleError::Host(HostExtensionError::Internal(e.to_string())))?;
221
222            result.map_err(wit_role_error_to_host_v03)
223        } else {
224            use crate::host_bindings::exports::greentic::extension_design0_2_0::roles::{
225                CompileContext as WitCompileContext, RoleError as WitRoleError,
226                TargetKind as WitTargetKind,
227            };
228
229            let func = instance
230                .get_typed_func::<
231                    (String, WitTargetKind, String, Option<WitCompileContext>),
232                    (Result<String, WitRoleError>,),
233                >(&mut store, &func_idx)
234                .map_err(|e| RoleError::Host(HostExtensionError::Internal(e.to_string())))?;
235
236            let wit_ctx = ctx.cloned().map(|c| WitCompileContext {
237                flow_entries_json: c.flow_entries_json,
238                flow_id: c.flow_id,
239                locale: c.locale,
240            });
241
242            let (result,) = func
243                .call(
244                    &mut store,
245                    (
246                        name.to_string(),
247                        target_to_wit(target),
248                        entry_json.to_string(),
249                        wit_ctx,
250                    ),
251                )
252                .map_err(|e| RoleError::Host(HostExtensionError::Internal(e.to_string())))?;
253
254            result.map_err(wit_role_error_to_host)
255        }
256    }
257}
258
259fn wit_role_spec_to_host(
260    s: crate::host_bindings::exports::greentic::extension_design0_2_0::roles::RoleSpec,
261) -> RoleSpec {
262    RoleSpec {
263        name: s.name,
264        description: s.description,
265        json_schema: s.json_schema,
266        target: target_from_wit(s.target),
267        schema_version: s.schema_version,
268        context_aware: s.context_aware,
269    }
270}
271
272fn wit_diagnostic_to_host(
273    d: crate::host_bindings::exports::greentic::extension_design0_2_0::roles::Diagnostic,
274) -> Diagnostic {
275    use crate::host_bindings::greentic::extension_base0_1_0::types::Severity as WitSeverity;
276    Diagnostic {
277        severity: match d.severity {
278            WitSeverity::Error => Severity::Error,
279            WitSeverity::Warning => Severity::Warning,
280            WitSeverity::Info => Severity::Info,
281            WitSeverity::Hint => Severity::Hint,
282        },
283        code: d.code,
284        message: d.message,
285        path: d.path,
286    }
287}
288
289// ---------------------------------------------------------------------------
290// v0.2.0 error mapping helpers
291// ---------------------------------------------------------------------------
292
293fn wit_role_error_to_host(
294    e: crate::host_bindings::exports::greentic::extension_design0_2_0::roles::RoleError,
295) -> RoleError {
296    use crate::host_bindings::exports::greentic::extension_design0_2_0::roles::RoleError as WitRoleError;
297    match e {
298        WitRoleError::UnknownRole(s) => RoleError::UnknownRole(s),
299        WitRoleError::InvalidInput(diags) => {
300            RoleError::InvalidInput(diags.into_iter().map(wit_diagnostic_to_host).collect())
301        }
302        WitRoleError::CompileFailed(s) => RoleError::CompileFailed(s),
303        WitRoleError::TargetNotSupported(t) => RoleError::TargetNotSupported(target_from_wit(t)),
304        WitRoleError::VersionNotSupported(v) => RoleError::VersionNotSupported(v),
305        WitRoleError::Host(ee) => RoleError::Host(wit_extension_error_to_host(ee)),
306    }
307}
308
309fn wit_extension_error_to_host(
310    e: crate::host_bindings::exports::greentic::extension_design0_2_0::roles::ExtensionError,
311) -> HostExtensionError {
312    use crate::host_bindings::exports::greentic::extension_design0_2_0::roles::ExtensionError as WitErr;
313    match e {
314        WitErr::InvalidInput(s) => HostExtensionError::InvalidInput(s),
315        WitErr::MissingCapability(s) => HostExtensionError::MissingCapability(s),
316        WitErr::PermissionDenied(s) => HostExtensionError::PermissionDenied(s),
317        WitErr::Internal(s) => HostExtensionError::Internal(s),
318    }
319}
320
321fn target_to_wit(
322    t: TargetKind,
323) -> crate::host_bindings::exports::greentic::extension_design0_2_0::roles::TargetKind {
324    use crate::host_bindings::exports::greentic::extension_design0_2_0::roles::TargetKind as Wit;
325    match t {
326        TargetKind::AdaptiveCard => Wit::AdaptiveCard,
327        TargetKind::SlackBlockKit => Wit::SlackBlockKit,
328        TargetKind::TeamsCard => Wit::TeamsCard,
329        TargetKind::PlainText => Wit::PlainText,
330    }
331}
332
333fn target_from_wit(
334    t: crate::host_bindings::exports::greentic::extension_design0_2_0::roles::TargetKind,
335) -> TargetKind {
336    use crate::host_bindings::exports::greentic::extension_design0_2_0::roles::TargetKind as Wit;
337    match t {
338        Wit::AdaptiveCard => TargetKind::AdaptiveCard,
339        Wit::SlackBlockKit => TargetKind::SlackBlockKit,
340        Wit::TeamsCard => TargetKind::TeamsCard,
341        Wit::PlainText => TargetKind::PlainText,
342    }
343}
344
345// ---------------------------------------------------------------------------
346// v0.3.0 error mapping helpers
347// ---------------------------------------------------------------------------
348
349fn wit_role_error_to_host_v03(
350    e: crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::RoleError,
351) -> RoleError {
352    use crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::RoleError as WitRoleError;
353    match e {
354        WitRoleError::UnknownRole(s) => RoleError::UnknownRole(s),
355        WitRoleError::InvalidInput(diags) => {
356            RoleError::InvalidInput(diags.into_iter().map(wit_diagnostic_to_host_v03).collect())
357        }
358        WitRoleError::CompileFailed(s) => RoleError::CompileFailed(s),
359        WitRoleError::TargetNotSupported(t) => {
360            RoleError::TargetNotSupported(target_from_wit_v03(t))
361        }
362        WitRoleError::VersionNotSupported(v) => RoleError::VersionNotSupported(v),
363        WitRoleError::Host(ee) => RoleError::Host(wit_extension_error_to_host_v03(ee)),
364    }
365}
366
367/// Map the 6-variant `extension-error` from the `design_v03` roles re-export.
368///
369/// This is structurally identical to `crate::ext_error::from_design_v03` but
370/// takes the distinct re-exported type generated under the roles interface
371/// path rather than the base-types path.
372fn wit_extension_error_to_host_v03(
373    e: crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::ExtensionError,
374) -> HostExtensionError {
375    use crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::ExtensionError as WitErr;
376    match e {
377        WitErr::InvalidInput(s) => HostExtensionError::InvalidInput(s),
378        WitErr::MissingCapability(s) => HostExtensionError::MissingCapability(s),
379        WitErr::PermissionDenied(s) => HostExtensionError::PermissionDenied(s),
380        WitErr::NotFound(s) => HostExtensionError::NotFound(s),
381        WitErr::SchemaInvalid(s) => HostExtensionError::SchemaInvalid(s),
382        WitErr::Internal(s) => HostExtensionError::Internal(s),
383    }
384}
385
386fn wit_diagnostic_to_host_v03(
387    d: crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::Diagnostic,
388) -> Diagnostic {
389    use crate::host_bindings::design_v03::greentic::extension_base0_2_0::types::Severity as WitSeverity;
390    Diagnostic {
391        severity: match d.severity {
392            WitSeverity::Error => Severity::Error,
393            WitSeverity::Warning => Severity::Warning,
394            WitSeverity::Info => Severity::Info,
395            WitSeverity::Hint => Severity::Hint,
396        },
397        code: d.code,
398        message: d.message,
399        path: d.path,
400    }
401}
402
403fn target_to_wit_v03(
404    t: TargetKind,
405) -> crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::TargetKind {
406    use crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::TargetKind as Wit;
407    match t {
408        TargetKind::AdaptiveCard => Wit::AdaptiveCard,
409        TargetKind::SlackBlockKit => Wit::SlackBlockKit,
410        TargetKind::TeamsCard => Wit::TeamsCard,
411        TargetKind::PlainText => Wit::PlainText,
412    }
413}
414
415fn target_from_wit_v03(
416    t: crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::TargetKind,
417) -> TargetKind {
418    use crate::host_bindings::design_v03::exports::greentic::extension_design0_3_0::roles::TargetKind as Wit;
419    match t {
420        Wit::AdaptiveCard => TargetKind::AdaptiveCard,
421        Wit::SlackBlockKit => TargetKind::SlackBlockKit,
422        Wit::TeamsCard => TargetKind::TeamsCard,
423        Wit::PlainText => TargetKind::PlainText,
424    }
425}