1mod config;
6pub mod describe;
7mod error;
8mod path_safety;
9mod resolve;
10pub mod router;
11pub mod runner;
12mod store;
13#[cfg(test)]
14mod test_support;
15mod verify;
16
17pub use config::{DynSecretsStore, ExecConfig, RuntimePolicy, SecretsStore, VerifyPolicy};
18pub use error::{ExecError, RunnerError};
19pub use store::{ToolInfo, ToolStore};
20
21use greentic_types::TenantCtx;
22use serde_json::{Value, json};
23
24use crate::runner::Runner;
25
26#[derive(Clone, Debug)]
28pub struct ToolDef {
29 pub name: String,
30 pub description: String,
31 pub input_schema: Value,
32}
33
34#[derive(Clone, Debug)]
35pub struct ExecRequest {
36 pub component: String,
37 pub action: String,
38 pub args: Value,
39 pub tenant: Option<TenantCtx>,
40}
41
42pub fn exec(req: ExecRequest, cfg: &ExecConfig) -> Result<Value, ExecError> {
47 let resolved = resolve::resolve(&req.component, &cfg.store)
48 .map_err(|err| ExecError::resolve(&req.component, err))?;
49
50 let verified = verify::verify(&req.component, resolved, &cfg.security)
51 .map_err(|err| ExecError::verification(&req.component, err))?;
52
53 let runner = runner::DefaultRunner::new(&cfg.runtime)
54 .map_err(|err| ExecError::runner(&req.component, err))?;
55
56 let result = runner.run(
57 &req,
58 &verified,
59 runner::ExecutionContext {
60 runtime: &cfg.runtime,
61 http_enabled: cfg.http_enabled,
62 secrets_store: cfg.secrets_store.clone(),
63 },
64 );
65
66 let value = match result {
67 Ok(v) => v,
68 Err(RunnerError::ActionNotFound { .. }) => {
69 return Err(ExecError::not_found(
70 req.component.clone(),
71 req.action.clone(),
72 ));
73 }
74 Err(RunnerError::ToolTransient { component, message }) => {
75 return Err(ExecError::tool_error(
76 component,
77 req.action.clone(),
78 "transient",
79 json!({ "message": message }),
80 ));
81 }
82 Err(RunnerError::Internal(message)) => {
83 return Err(ExecError::runner(
84 &req.component,
85 RunnerError::Internal(message),
86 ));
87 }
88 Err(err) => return Err(ExecError::runner(&req.component, err)),
89 };
90
91 if let Some(code) = value
92 .get("error")
93 .and_then(|error| error.get("code"))
94 .and_then(Value::as_str)
95 .map(str::to_owned)
96 {
97 if code == "iface-error.not-found" {
98 return Err(ExecError::not_found(req.component, req.action));
99 } else {
100 return Err(ExecError::tool_error(
101 req.component,
102 req.action,
103 code,
104 value,
105 ));
106 }
107 }
108
109 Ok(value)
110}
111
112pub fn list_tools(component: &str, cfg: &ExecConfig) -> Result<Vec<ToolDef>, ExecError> {
118 let resolved = resolve::resolve(component, &cfg.store)
119 .map_err(|err| ExecError::resolve(component, err))?;
120 let verified = verify::verify(component, resolved, &cfg.security)
121 .map_err(|err| ExecError::verification(component, err))?;
122
123 let runner = runner::DefaultRunner::new(&cfg.runtime)
124 .map_err(|err| ExecError::runner(component, err))?;
125
126 let tools = runner
127 .list_tools_router(
128 &verified,
129 runner::ExecutionContext {
130 runtime: &cfg.runtime,
131 http_enabled: cfg.http_enabled,
132 secrets_store: cfg.secrets_store.clone(),
133 },
134 )
135 .map_err(|err| ExecError::runner(component, err))?
136 .unwrap_or_default();
137
138 Ok(tools
139 .into_iter()
140 .map(|t| ToolDef {
141 name: t.name,
142 description: t.description,
143 input_schema: serde_json::from_str(&t.input_schema)
144 .unwrap_or_else(|_| serde_json::json!({})),
145 })
146 .collect())
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::config::{RuntimePolicy, VerifyPolicy};
153 use crate::error::RunnerError;
154 use crate::store::ToolStore;
155 use serde_json::json;
156 use std::collections::HashMap;
157 use std::path::PathBuf;
158
159 use crate::verify::VerifiedArtifact;
160
161 #[derive(Default)]
162 struct MockRunner;
163
164 impl runner::Runner for MockRunner {
165 fn run(
166 &self,
167 request: &ExecRequest,
168 artifact: &VerifiedArtifact,
169 _ctx: runner::ExecutionContext<'_>,
170 ) -> Result<Value, RunnerError> {
171 let mut payload = request.args.clone();
172 if let Value::Object(map) = &mut payload {
173 map.insert(
174 "component_digest".to_string(),
175 Value::String(artifact.resolved.digest.clone()),
176 );
177 }
178 Ok(payload)
179 }
180 }
181
182 #[test]
183 fn local_resolve_and_verify_success() {
184 let tempdir = tempfile::tempdir().expect("tempdir");
185 let wasm_path = tempdir.path().join("echo.component.wasm");
186 std::fs::write(&wasm_path, b"fake wasm contents").expect("write");
187
188 let digest = crate::resolve::resolve(
189 "echo.component",
190 &ToolStore::LocalDir(PathBuf::from(tempdir.path())),
191 )
192 .expect("resolve")
193 .digest;
194
195 let mut required = HashMap::new();
196 required.insert("echo.component".to_string(), digest.clone());
197
198 let cfg = ExecConfig {
199 store: ToolStore::LocalDir(PathBuf::from(tempdir.path())),
200 security: VerifyPolicy {
201 allow_unverified: false,
202 required_digests: required,
203 trusted_signers: Vec::new(),
204 },
205 runtime: RuntimePolicy::default(),
206 http_enabled: false,
207 secrets_store: None,
208 };
209
210 let req = ExecRequest {
211 component: "echo.component".into(),
212 action: "noop".into(),
213 args: json!({"message": "hello"}),
214 tenant: None,
215 };
216
217 let resolved =
219 crate::resolve::resolve(&req.component, &cfg.store).expect("resolve second time");
220 let verified =
221 crate::verify::verify(&req.component, resolved, &cfg.security).expect("verify");
222 let result = MockRunner
223 .run(
224 &req,
225 &verified,
226 runner::ExecutionContext {
227 runtime: &cfg.runtime,
228 http_enabled: cfg.http_enabled,
229 secrets_store: cfg.secrets_store.clone(),
230 },
231 )
232 .expect("run");
233
234 assert_eq!(
235 result.get("component_digest").and_then(Value::as_str),
236 Some(digest.as_str())
237 );
238 }
239}