1use anyhow::{Context, Result};
2use serde_json::Value;
3
4use crate::{ExecConfig, ExecError, ExecRequest, exec};
5
6#[cfg(feature = "describe-v1")]
7const DESCRIBE_INTERFACE: &str = "greentic:component/describe-v1@1.0.0";
8#[cfg(feature = "describe-v1")]
9const DESCRIBE_EXPORT: &str = "greentic:component/describe-v1@1.0.0#describe-json";
10
11#[derive(Debug)]
12pub enum Maybe<T> {
13 Data(T),
14 Unsupported,
15}
16
17#[derive(Debug)]
18pub struct ToolDescribe {
19 pub describe_v1: Option<Value>,
20 pub capabilities: Maybe<Vec<String>>,
21 pub secrets: Maybe<Value>,
22 pub config_schema: Maybe<Value>,
23}
24
25pub fn describe_tool(name: &str, cfg: &ExecConfig) -> Result<ToolDescribe> {
26 #[cfg(feature = "describe-v1")]
27 {
28 if let Some(document) = try_describe_v1(name, cfg)? {
29 return Ok(ToolDescribe {
30 describe_v1: Some(document),
31 capabilities: Maybe::Unsupported,
32 secrets: Maybe::Unsupported,
33 config_schema: Maybe::Unsupported,
34 });
35 }
36 }
37
38 fn try_action(name: &str, action: &str, cfg: &ExecConfig) -> Result<Maybe<Value>> {
39 let req = ExecRequest {
40 component: name.to_string(),
41 action: action.to_string(),
42 args: Value::Object(Default::default()),
43 tenant: None,
44 };
45
46 match exec(req, cfg) {
47 Ok(v) => Ok(Maybe::Data(v)),
48 Err(ExecError::NotFound { .. }) => Ok(Maybe::Unsupported),
49 Err(ExecError::Tool { code, payload, .. }) if code == "iface-error.not-found" => {
50 let _ = payload;
51 Ok(Maybe::Unsupported)
52 }
53 Err(e) => Err(e.into()),
54 }
55 }
56
57 let capabilities_value = try_action(name, "capabilities", cfg)?;
58 let secrets = try_action(name, "list_secrets", cfg)?;
59 let config_schema = try_action(name, "config_schema", cfg)?;
60
61 let capabilities = match capabilities_value {
62 Maybe::Data(value) => {
63 if let Some(arr) = value.as_array() {
64 let list = arr
65 .iter()
66 .filter_map(|v| v.as_str().map(|s| s.to_string()))
67 .collect::<Vec<_>>();
68 Maybe::Data(list)
69 } else {
70 Maybe::Data(Vec::new())
71 }
72 }
73 Maybe::Unsupported => Maybe::Unsupported,
74 };
75
76 Ok(ToolDescribe {
77 describe_v1: None,
78 capabilities,
79 secrets,
80 config_schema,
81 })
82}
83
84#[cfg(feature = "describe-v1")]
85fn try_describe_v1(name: &str, cfg: &ExecConfig) -> Result<Option<Value>> {
86 use wasmtime::component::{Component, Linker};
87 use wasmtime::{Config, Engine, Store};
88
89 let resolved =
90 crate::resolve::resolve(name, &cfg.store).map_err(|err| ExecError::resolve(name, err))?;
91 let verified = crate::verify::verify(name, resolved, &cfg.security)
92 .map_err(|err| ExecError::verification(name, err))?;
93
94 let mut config = Config::new();
95 config.wasm_component_model(true);
96 config.async_support(false);
97 config.epoch_interruption(true);
98
99 let engine = Engine::new(&config)?;
100 let component = match Component::from_binary(&engine, verified.resolved.bytes.as_ref()) {
101 Ok(component) => component,
102 Err(_) => return Ok(None),
103 };
104 let linker = Linker::new(&engine);
105 let mut store = Store::new(&engine, ());
106
107 let instance = match linker.instantiate(&mut store, &component) {
108 Ok(instance) => instance,
109 Err(_) => return Ok(None),
110 };
111 if instance
112 .get_export(&mut store, None, DESCRIBE_INTERFACE)
113 .is_none()
114 {
115 return Ok(None);
116 }
117
118 let func = match instance.get_typed_func::<(), (String,)>(&mut store, DESCRIBE_EXPORT) {
119 Ok(func) => func,
120 Err(err) => {
121 let msg = err.to_string();
122 if msg.contains("unknown export") {
123 return Ok(None);
124 }
125 return Err(err);
126 }
127 };
128
129 let (raw,) = func.call(&mut store, ())?;
130 let value: Value =
131 serde_json::from_str(&raw).with_context(|| "describe-json returned invalid JSON")?;
132 Ok(Some(value))
133}