1use std::collections::HashMap;
2use std::fs::File;
3use std::io::Read;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::sync::Arc;
7
8use crate::component_api::component::greentic::component::control::Host as ComponentControlHost;
9use crate::component_api::{
10 ComponentPre, control, node::ExecCtx as ComponentExecCtx, node::InvokeResult, node::NodeError,
11};
12use crate::oauth::{OAuthBrokerConfig, OAuthBrokerHost, OAuthHostContext};
13use crate::runtime_wasmtime::{Component, Engine, Linker, ResourceTable};
14use anyhow::{Context, Result, anyhow, bail};
15use greentic_interfaces_wasmtime::host_helpers::v1::{
16 HostFns, add_all_v1_to_linker,
17 http_client::{
18 HttpClientError, HttpClientHost, Request as HttpRequest, Response as HttpResponse,
19 TenantCtx as HttpTenantCtx,
20 },
21 messaging_session::{
22 MessagingSessionError as MessagingError, MessagingSessionHost, OpAck as MessagingAck,
23 OutboundMessage, TenantCtx as MessagingTenantCtx,
24 },
25 runner_host_http::RunnerHostHttp,
26 runner_host_kv::RunnerHostKv,
27 secrets_store::{SecretsError, SecretsStoreHost},
28 state_store::{
29 OpAck as StateOpAck, StateKey as HostStateKey, StateStoreError as StateError,
30 StateStoreHost, TenantCtx as StateTenantCtx,
31 },
32 telemetry_logger::{
33 OpAck as TelemetryAck, SpanContext as TelemetrySpanContext,
34 TelemetryLoggerError as TelemetryError, TelemetryLoggerHost,
35 TenantCtx as TelemetryTenantCtx,
36 },
37};
38use greentic_pack::builder as legacy_pack;
39use greentic_types::{
40 EnvId, Flow, StateKey as StoreStateKey, TeamId, TenantCtx as TypesTenantCtx, TenantId, UserId,
41 decode_pack_manifest,
42};
43use once_cell::sync::Lazy;
44use parking_lot::Mutex;
45use reqwest::blocking::Client as BlockingClient;
46use runner_core::normalize_under_root;
47use serde::{Deserialize, Serialize};
48use serde_cbor;
49use serde_json::{self, Value};
50use tokio::fs;
51use wasmparser::{Parser, Payload};
52use wasmtime::StoreContextMut;
53use zip::ZipArchive;
54
55use crate::runner::engine::{FlowContext, FlowEngine, FlowStatus};
56use crate::runner::flow_adapter::{FlowIR, flow_doc_to_ir, flow_ir_to_flow};
57use crate::runner::mocks::{HttpDecision, HttpMockRequest, HttpMockResponse, MockLayer};
58
59use crate::config::HostConfig;
60use crate::secrets::{DynSecretsManager, read_secret_blocking};
61use crate::storage::state::STATE_PREFIX;
62use crate::storage::{DynSessionStore, DynStateStore};
63use crate::verify;
64use crate::wasi::RunnerWasiPolicy;
65use tracing::warn;
66use wasmtime_wasi::p2::add_to_linker_sync as add_wasi_to_linker;
67use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView};
68
69use greentic_flow::model::FlowDoc;
70
71#[allow(dead_code)]
72pub struct PackRuntime {
73 path: PathBuf,
75 archive_path: Option<PathBuf>,
77 config: Arc<HostConfig>,
78 engine: Engine,
79 metadata: PackMetadata,
80 manifest: Option<greentic_types::PackManifest>,
81 legacy_manifest: Option<Box<legacy_pack::PackManifest>>,
82 mocks: Option<Arc<MockLayer>>,
83 flows: Option<PackFlows>,
84 components: HashMap<String, PackComponent>,
85 http_client: Arc<BlockingClient>,
86 pre_cache: Mutex<HashMap<String, ComponentPre<ComponentState>>>,
87 session_store: Option<DynSessionStore>,
88 state_store: Option<DynStateStore>,
89 wasi_policy: Arc<RunnerWasiPolicy>,
90 secrets: DynSecretsManager,
91 oauth_config: Option<OAuthBrokerConfig>,
92}
93
94struct PackComponent {
95 #[allow(dead_code)]
96 name: String,
97 #[allow(dead_code)]
98 version: String,
99 component: Component,
100}
101
102fn build_blocking_client() -> BlockingClient {
103 std::thread::spawn(|| {
104 BlockingClient::builder()
105 .no_proxy()
106 .build()
107 .expect("blocking client")
108 })
109 .join()
110 .expect("client build thread panicked")
111}
112
113fn normalize_pack_path(path: &Path) -> Result<(PathBuf, PathBuf)> {
114 let (root, candidate) = if path.is_absolute() {
115 let parent = path
116 .parent()
117 .ok_or_else(|| anyhow!("pack path {} has no parent", path.display()))?;
118 let root = parent
119 .canonicalize()
120 .with_context(|| format!("failed to canonicalize {}", parent.display()))?;
121 let file = path
122 .file_name()
123 .ok_or_else(|| anyhow!("pack path {} has no file name", path.display()))?;
124 (root, PathBuf::from(file))
125 } else {
126 let cwd = std::env::current_dir().context("failed to resolve current directory")?;
127 let base = if let Some(parent) = path.parent() {
128 cwd.join(parent)
129 } else {
130 cwd
131 };
132 let root = base
133 .canonicalize()
134 .with_context(|| format!("failed to canonicalize {}", base.display()))?;
135 let file = path
136 .file_name()
137 .ok_or_else(|| anyhow!("pack path {} has no file name", path.display()))?;
138 (root, PathBuf::from(file))
139 };
140 let safe = normalize_under_root(&root, &candidate)?;
141 Ok((root, safe))
142}
143
144static HTTP_CLIENT: Lazy<Arc<BlockingClient>> = Lazy::new(|| Arc::new(build_blocking_client()));
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct FlowDescriptor {
148 pub id: String,
149 #[serde(rename = "type")]
150 pub flow_type: String,
151 pub profile: String,
152 pub version: String,
153 #[serde(default)]
154 pub description: Option<String>,
155}
156
157pub struct HostState {
158 config: Arc<HostConfig>,
159 http_client: Arc<BlockingClient>,
160 default_env: String,
161 #[allow(dead_code)]
162 session_store: Option<DynSessionStore>,
163 state_store: Option<DynStateStore>,
164 mocks: Option<Arc<MockLayer>>,
165 secrets: DynSecretsManager,
166 oauth_config: Option<OAuthBrokerConfig>,
167 oauth_host: OAuthBrokerHost,
168}
169
170impl HostState {
171 #[allow(clippy::default_constructed_unit_structs)]
172 pub fn new(
173 config: Arc<HostConfig>,
174 http_client: Arc<BlockingClient>,
175 mocks: Option<Arc<MockLayer>>,
176 session_store: Option<DynSessionStore>,
177 state_store: Option<DynStateStore>,
178 secrets: DynSecretsManager,
179 oauth_config: Option<OAuthBrokerConfig>,
180 ) -> Result<Self> {
181 let default_env = std::env::var("GREENTIC_ENV").unwrap_or_else(|_| "local".to_string());
182 Ok(Self {
183 config,
184 http_client,
185 default_env,
186 session_store,
187 state_store,
188 mocks,
189 secrets,
190 oauth_config,
191 oauth_host: OAuthBrokerHost::default(),
192 })
193 }
194
195 pub fn get_secret(&self, key: &str) -> Result<String> {
196 if !self.config.secrets_policy.is_allowed(key) {
197 bail!("secret {key} is not permitted by bindings policy");
198 }
199 if let Some(mock) = &self.mocks
200 && let Some(value) = mock.secrets_lookup(key)
201 {
202 return Ok(value);
203 }
204 let bytes = read_secret_blocking(&self.secrets, key)
205 .context("failed to read secret from manager")?;
206 let value = String::from_utf8(bytes).context("secret value is not valid UTF-8")?;
207 Ok(value)
208 }
209
210 fn tenant_ctx_from_v1(&self, ctx: Option<StateTenantCtx>) -> Result<TypesTenantCtx> {
211 let tenant_raw = ctx
212 .as_ref()
213 .map(|ctx| ctx.tenant.clone())
214 .unwrap_or_else(|| self.config.tenant.clone());
215 let env_raw = ctx
216 .as_ref()
217 .map(|ctx| ctx.env.clone())
218 .unwrap_or_else(|| self.default_env.clone());
219 let tenant_id = TenantId::from_str(&tenant_raw)
220 .with_context(|| format!("invalid tenant id `{tenant_raw}`"))?;
221 let env_id = EnvId::from_str(&env_raw)
222 .unwrap_or_else(|_| EnvId::from_str("local").expect("default env must be valid"));
223 let mut tenant_ctx = TypesTenantCtx::new(env_id, tenant_id);
224 if let Some(ctx) = ctx {
225 if let Some(team) = ctx.team.or(ctx.team_id) {
226 let team_id =
227 TeamId::from_str(&team).with_context(|| format!("invalid team id `{team}`"))?;
228 tenant_ctx = tenant_ctx.with_team(Some(team_id));
229 }
230 if let Some(user) = ctx.user.or(ctx.user_id) {
231 let user_id =
232 UserId::from_str(&user).with_context(|| format!("invalid user id `{user}`"))?;
233 tenant_ctx = tenant_ctx.with_user(Some(user_id));
234 }
235 if let Some(flow) = ctx.flow_id {
236 tenant_ctx = tenant_ctx.with_flow(flow);
237 }
238 if let Some(node) = ctx.node_id {
239 tenant_ctx = tenant_ctx.with_node(node);
240 }
241 if let Some(provider) = ctx.provider_id {
242 tenant_ctx = tenant_ctx.with_provider(provider);
243 }
244 if let Some(session) = ctx.session_id {
245 tenant_ctx = tenant_ctx.with_session(session);
246 }
247 tenant_ctx.trace_id = ctx.trace_id;
248 }
249 Ok(tenant_ctx)
250 }
251}
252
253impl SecretsStoreHost for HostState {
254 fn get(&mut self, key: String) -> Result<Option<Vec<u8>>, SecretsError> {
255 if !self.config.secrets_policy.is_allowed(&key) {
256 return Err(SecretsError::Denied);
257 }
258 if let Some(mock) = &self.mocks
259 && let Some(value) = mock.secrets_lookup(&key)
260 {
261 return Ok(Some(value.into_bytes()));
262 }
263 match read_secret_blocking(&self.secrets, &key) {
264 Ok(bytes) => Ok(Some(bytes)),
265 Err(err) => {
266 warn!(secret = %key, error = %err, "secret lookup failed");
267 Err(SecretsError::NotFound)
268 }
269 }
270 }
271}
272
273impl HttpClientHost for HostState {
274 fn send(
275 &mut self,
276 req: HttpRequest,
277 _ctx: Option<HttpTenantCtx>,
278 ) -> Result<HttpResponse, HttpClientError> {
279 if !self.config.http_enabled {
280 return Err(HttpClientError {
281 code: "denied".into(),
282 message: "http client disabled by policy".into(),
283 });
284 }
285
286 let mut mock_state = None;
287 let raw_body = req.body.clone();
288 if let Some(mock) = &self.mocks
289 && let Ok(meta) = HttpMockRequest::new(&req.method, &req.url, raw_body.as_deref())
290 {
291 match mock.http_begin(&meta) {
292 HttpDecision::Mock(response) => {
293 let headers = response
294 .headers
295 .iter()
296 .map(|(k, v)| (k.clone(), v.clone()))
297 .collect();
298 return Ok(HttpResponse {
299 status: response.status,
300 headers,
301 body: response.body.clone().map(|b| b.into_bytes()),
302 });
303 }
304 HttpDecision::Deny(reason) => {
305 return Err(HttpClientError {
306 code: "denied".into(),
307 message: reason,
308 });
309 }
310 HttpDecision::Passthrough { record } => {
311 mock_state = Some((meta, record));
312 }
313 }
314 }
315
316 let method = req.method.parse().unwrap_or(reqwest::Method::GET);
317 let mut builder = self.http_client.request(method, &req.url);
318 for (key, value) in req.headers {
319 if let Ok(header) = reqwest::header::HeaderName::from_bytes(key.as_bytes())
320 && let Ok(header_value) = reqwest::header::HeaderValue::from_str(&value)
321 {
322 builder = builder.header(header, header_value);
323 }
324 }
325
326 if let Some(body) = raw_body.clone() {
327 builder = builder.body(body);
328 }
329
330 let response = match builder.send() {
331 Ok(resp) => resp,
332 Err(err) => {
333 warn!(url = %req.url, error = %err, "http client request failed");
334 return Err(HttpClientError {
335 code: "unavailable".into(),
336 message: err.to_string(),
337 });
338 }
339 };
340
341 let status = response.status().as_u16();
342 let headers_vec = response
343 .headers()
344 .iter()
345 .map(|(k, v)| {
346 (
347 k.as_str().to_string(),
348 v.to_str().unwrap_or_default().to_string(),
349 )
350 })
351 .collect::<Vec<_>>();
352 let body_bytes = response.bytes().ok().map(|b| b.to_vec());
353
354 if let Some((meta, true)) = mock_state.take()
355 && let Some(mock) = &self.mocks
356 {
357 let recorded = HttpMockResponse::new(
358 status,
359 headers_vec.clone().into_iter().collect(),
360 body_bytes
361 .as_ref()
362 .map(|b| String::from_utf8_lossy(b).into_owned()),
363 );
364 mock.http_record(&meta, &recorded);
365 }
366
367 Ok(HttpResponse {
368 status,
369 headers: headers_vec,
370 body: body_bytes,
371 })
372 }
373}
374
375impl StateStoreHost for HostState {
376 fn read(
377 &mut self,
378 key: HostStateKey,
379 ctx: Option<StateTenantCtx>,
380 ) -> Result<Vec<u8>, StateError> {
381 let store = match self.state_store.as_ref() {
382 Some(store) => store.clone(),
383 None => {
384 return Err(StateError {
385 code: "unavailable".into(),
386 message: "state store not configured".into(),
387 });
388 }
389 };
390 let tenant_ctx = match self.tenant_ctx_from_v1(ctx) {
391 Ok(ctx) => ctx,
392 Err(err) => {
393 return Err(StateError {
394 code: "invalid-ctx".into(),
395 message: err.to_string(),
396 });
397 }
398 };
399 let key = StoreStateKey::from(key);
400 match store.get_json(&tenant_ctx, STATE_PREFIX, &key, None) {
401 Ok(Some(value)) => Ok(serde_json::to_vec(&value).unwrap_or_else(|_| Vec::new())),
402 Ok(None) => Err(StateError {
403 code: "not_found".into(),
404 message: "state key not found".into(),
405 }),
406 Err(err) => Err(StateError {
407 code: "internal".into(),
408 message: err.to_string(),
409 }),
410 }
411 }
412
413 fn write(
414 &mut self,
415 key: HostStateKey,
416 bytes: Vec<u8>,
417 ctx: Option<StateTenantCtx>,
418 ) -> Result<StateOpAck, StateError> {
419 let store = match self.state_store.as_ref() {
420 Some(store) => store.clone(),
421 None => {
422 return Err(StateError {
423 code: "unavailable".into(),
424 message: "state store not configured".into(),
425 });
426 }
427 };
428 let tenant_ctx = match self.tenant_ctx_from_v1(ctx) {
429 Ok(ctx) => ctx,
430 Err(err) => {
431 return Err(StateError {
432 code: "invalid-ctx".into(),
433 message: err.to_string(),
434 });
435 }
436 };
437 let key = StoreStateKey::from(key);
438 let value = serde_json::from_slice(&bytes)
439 .unwrap_or_else(|_| Value::String(String::from_utf8_lossy(&bytes).to_string()));
440 match store.set_json(&tenant_ctx, STATE_PREFIX, &key, None, &value, None) {
441 Ok(()) => Ok(StateOpAck::Ok),
442 Err(err) => Err(StateError {
443 code: "internal".into(),
444 message: err.to_string(),
445 }),
446 }
447 }
448
449 fn delete(
450 &mut self,
451 key: HostStateKey,
452 ctx: Option<StateTenantCtx>,
453 ) -> Result<StateOpAck, StateError> {
454 let store = match self.state_store.as_ref() {
455 Some(store) => store.clone(),
456 None => {
457 return Err(StateError {
458 code: "unavailable".into(),
459 message: "state store not configured".into(),
460 });
461 }
462 };
463 let tenant_ctx = match self.tenant_ctx_from_v1(ctx) {
464 Ok(ctx) => ctx,
465 Err(err) => {
466 return Err(StateError {
467 code: "invalid-ctx".into(),
468 message: err.to_string(),
469 });
470 }
471 };
472 let key = StoreStateKey::from(key);
473 match store.del(&tenant_ctx, STATE_PREFIX, &key) {
474 Ok(_) => Ok(StateOpAck::Ok),
475 Err(err) => Err(StateError {
476 code: "internal".into(),
477 message: err.to_string(),
478 }),
479 }
480 }
481}
482
483impl TelemetryLoggerHost for HostState {
484 fn log(
485 &mut self,
486 span: TelemetrySpanContext,
487 fields: Vec<(String, String)>,
488 _ctx: Option<TelemetryTenantCtx>,
489 ) -> Result<TelemetryAck, TelemetryError> {
490 if let Some(mock) = &self.mocks
491 && mock.telemetry_drain(&[("span_json", span.flow_id.as_str())])
492 {
493 return Ok(TelemetryAck::Ok);
494 }
495 let mut map = serde_json::Map::new();
496 for (k, v) in fields {
497 map.insert(k, Value::String(v));
498 }
499 tracing::info!(
500 tenant = %span.tenant,
501 flow_id = %span.flow_id,
502 node = ?span.node_id,
503 provider = %span.provider,
504 fields = %serde_json::Value::Object(map.clone()),
505 "telemetry log from pack"
506 );
507 Ok(TelemetryAck::Ok)
508 }
509}
510
511impl RunnerHostHttp for HostState {
512 fn request(
513 &mut self,
514 method: String,
515 url: String,
516 headers: Vec<String>,
517 body: Option<Vec<u8>>,
518 ) -> Result<Vec<u8>, String> {
519 let req = HttpRequest {
520 method,
521 url,
522 headers: headers
523 .chunks(2)
524 .filter_map(|chunk| {
525 if chunk.len() == 2 {
526 Some((chunk[0].clone(), chunk[1].clone()))
527 } else {
528 None
529 }
530 })
531 .collect(),
532 body,
533 };
534 match HttpClientHost::send(self, req, None) {
535 Ok(resp) => Ok(resp.body.unwrap_or_default()),
536 Err(err) => Err(err.message),
537 }
538 }
539}
540
541impl RunnerHostKv for HostState {
542 fn get(&mut self, _ns: String, _key: String) -> Option<String> {
543 None
544 }
545
546 fn put(&mut self, _ns: String, _key: String, _val: String) {}
547}
548
549impl MessagingSessionHost for HostState {
550 fn send(
551 &mut self,
552 _message: OutboundMessage,
553 _ctx: MessagingTenantCtx,
554 ) -> Result<MessagingAck, MessagingError> {
555 Err(MessagingError {
556 code: "unimplemented".into(),
557 message: "messaging session host not wired".into(),
558 })
559 }
560}
561
562enum ManifestLoad {
563 New {
564 manifest: Box<greentic_types::PackManifest>,
565 flows: PackFlows,
566 },
567 Legacy {
568 manifest: Box<legacy_pack::PackManifest>,
569 flows: PackFlows,
570 },
571}
572
573fn load_manifest_and_flows(path: &Path) -> Result<ManifestLoad> {
574 let mut archive = ZipArchive::new(File::open(path)?)
575 .with_context(|| format!("{} is not a valid gtpack", path.display()))?;
576 let bytes = read_entry(&mut archive, "manifest.cbor")
577 .with_context(|| format!("missing manifest.cbor in {}", path.display()))?;
578 match decode_pack_manifest(&bytes) {
579 Ok(manifest) => {
580 let cache = PackFlows::from_manifest(manifest.clone());
581 Ok(ManifestLoad::New {
582 manifest: Box::new(manifest),
583 flows: cache,
584 })
585 }
586 Err(err) => {
587 tracing::debug!(error = %err, pack = %path.display(), "decode_pack_manifest failed; trying legacy manifest");
588 let legacy: legacy_pack::PackManifest = serde_cbor::from_slice(&bytes)
590 .context("failed to decode legacy pack manifest from manifest.cbor")?;
591 let flows = load_legacy_flows(&mut archive, &legacy)?;
592 Ok(ManifestLoad::Legacy {
593 manifest: Box::new(legacy),
594 flows,
595 })
596 }
597 }
598}
599
600fn load_legacy_flows(
601 archive: &mut ZipArchive<File>,
602 manifest: &legacy_pack::PackManifest,
603) -> Result<PackFlows> {
604 let mut flows = HashMap::new();
605 let mut descriptors = Vec::new();
606
607 for entry in &manifest.flows {
608 let bytes = read_entry(archive, &entry.file_json)
609 .with_context(|| format!("missing flow json {}", entry.file_json))?;
610 let doc: FlowDoc = serde_json::from_slice(&bytes)
611 .with_context(|| format!("failed to decode flow doc {}", entry.file_json))?;
612 let normalized = normalize_flow_doc(doc);
613 let flow_ir = flow_doc_to_ir(normalized)?;
614 let flow = flow_ir_to_flow(flow_ir)?;
615
616 descriptors.push(FlowDescriptor {
617 id: entry.id.clone(),
618 flow_type: entry.kind.clone(),
619 profile: manifest.meta.pack_id.clone(),
620 version: manifest.meta.version.to_string(),
621 description: None,
622 });
623 flows.insert(entry.id.clone(), flow);
624 }
625
626 let mut entry_flows = manifest.meta.entry_flows.clone();
627 if entry_flows.is_empty() {
628 entry_flows = manifest.flows.iter().map(|f| f.id.clone()).collect();
629 }
630 let metadata = PackMetadata {
631 pack_id: manifest.meta.pack_id.clone(),
632 version: manifest.meta.version.to_string(),
633 entry_flows,
634 secret_requirements: Vec::new(),
635 };
636
637 Ok(PackFlows {
638 descriptors,
639 flows,
640 metadata,
641 })
642}
643
644pub struct ComponentState {
645 pub host: HostState,
646 wasi_ctx: WasiCtx,
647 resource_table: ResourceTable,
648}
649
650impl ComponentState {
651 pub fn new(host: HostState, policy: Arc<RunnerWasiPolicy>) -> Result<Self> {
652 let wasi_ctx = policy
653 .instantiate()
654 .context("failed to build WASI context")?;
655 Ok(Self {
656 host,
657 wasi_ctx,
658 resource_table: ResourceTable::new(),
659 })
660 }
661
662 fn host_mut(&mut self) -> &mut HostState {
663 &mut self.host
664 }
665}
666
667impl control::Host for ComponentState {
668 fn should_cancel(&mut self) -> bool {
669 false
670 }
671
672 fn yield_now(&mut self) {
673 }
675}
676
677fn add_component_control_to_linker(linker: &mut Linker<ComponentState>) -> wasmtime::Result<()> {
678 let mut inst = linker.instance("greentic:component/control@0.4.0")?;
679 inst.func_wrap(
680 "should-cancel",
681 |mut caller: StoreContextMut<'_, ComponentState>, (): ()| {
682 let host = caller.data_mut();
683 Ok((ComponentControlHost::should_cancel(host),))
684 },
685 )?;
686 inst.func_wrap(
687 "yield-now",
688 |mut caller: StoreContextMut<'_, ComponentState>, (): ()| {
689 let host = caller.data_mut();
690 ComponentControlHost::yield_now(host);
691 Ok(())
692 },
693 )?;
694 Ok(())
695}
696
697pub fn register_all(linker: &mut Linker<ComponentState>) -> Result<()> {
698 add_wasi_to_linker(linker)?;
699 add_all_v1_to_linker(
700 linker,
701 HostFns {
702 http_client: Some(|state| state.host_mut()),
703 oauth_broker: None,
704 runner_host_http: Some(|state| state.host_mut()),
705 runner_host_kv: Some(|state| state.host_mut()),
706 messaging_session: Some(|state| state.host_mut()),
707 telemetry_logger: Some(|state| state.host_mut()),
708 state_store: Some(|state| state.host_mut()),
709 secrets_store: Some(|state| state.host_mut()),
710 },
711 )?;
712 Ok(())
713}
714
715impl OAuthHostContext for ComponentState {
716 fn tenant_id(&self) -> &str {
717 &self.host.config.tenant
718 }
719
720 fn env(&self) -> &str {
721 &self.host.default_env
722 }
723
724 fn oauth_broker_host(&mut self) -> &mut OAuthBrokerHost {
725 &mut self.host.oauth_host
726 }
727
728 fn oauth_config(&self) -> Option<&OAuthBrokerConfig> {
729 self.host.oauth_config.as_ref()
730 }
731}
732
733impl WasiView for ComponentState {
734 fn ctx(&mut self) -> WasiCtxView<'_> {
735 WasiCtxView {
736 ctx: &mut self.wasi_ctx,
737 table: &mut self.resource_table,
738 }
739 }
740}
741
742#[allow(unsafe_code)]
743unsafe impl Send for ComponentState {}
744#[allow(unsafe_code)]
745unsafe impl Sync for ComponentState {}
746
747impl PackRuntime {
748 #[allow(clippy::too_many_arguments)]
749 pub async fn load(
750 path: impl AsRef<Path>,
751 config: Arc<HostConfig>,
752 mocks: Option<Arc<MockLayer>>,
753 archive_source: Option<&Path>,
754 session_store: Option<DynSessionStore>,
755 state_store: Option<DynStateStore>,
756 wasi_policy: Arc<RunnerWasiPolicy>,
757 secrets: DynSecretsManager,
758 oauth_config: Option<OAuthBrokerConfig>,
759 verify_archive: bool,
760 ) -> Result<Self> {
761 let path = path.as_ref();
762 let (_pack_root, safe_path) = normalize_pack_path(path)?;
763 let is_component = safe_path
764 .extension()
765 .and_then(|ext| ext.to_str())
766 .map(|ext| ext.eq_ignore_ascii_case("wasm"))
767 .unwrap_or(false);
768 let archive_hint_path = if let Some(source) = archive_source {
769 let (_, normalized) = normalize_pack_path(source)?;
770 Some(normalized)
771 } else if is_component {
772 None
773 } else {
774 Some(safe_path.clone())
775 };
776 let archive_hint = archive_hint_path.as_deref();
777 if verify_archive {
778 let verify_target = archive_hint.unwrap_or(&safe_path);
779 verify::verify_pack(verify_target).await?;
780 tracing::info!(pack_path = %verify_target.display(), "pack verification complete");
781 }
782 let engine = Engine::default();
783 let wasm_bytes = fs::read(&safe_path).await?;
784 let mut metadata = PackMetadata::from_wasm(&wasm_bytes)
785 .unwrap_or_else(|| PackMetadata::fallback(&safe_path));
786 let mut manifest = None;
787 let mut legacy_manifest: Option<Box<legacy_pack::PackManifest>> = None;
788 let flows = if let Some(archive_path) = archive_hint {
789 match load_manifest_and_flows(archive_path) {
790 Ok(ManifestLoad::New {
791 manifest: m,
792 flows: cache,
793 }) => {
794 metadata = cache.metadata.clone();
795 manifest = Some(*m);
796 Some(cache)
797 }
798 Ok(ManifestLoad::Legacy {
799 manifest: m,
800 flows: cache,
801 }) => {
802 metadata = cache.metadata.clone();
803 legacy_manifest = Some(m);
804 Some(cache)
805 }
806 Err(err) => {
807 warn!(error = %err, pack = %archive_path.display(), "failed to parse pack manifest; skipping flows");
808 None
809 }
810 }
811 } else {
812 None
813 };
814 let components = if let Some(archive_path) = archive_hint {
815 if let Some(new_manifest) = manifest.as_ref() {
816 match load_components_from_archive(&engine, archive_path, Some(new_manifest)) {
817 Ok(map) => map,
818 Err(err) => {
819 warn!(error = %err, pack = %archive_path.display(), "failed to load components from archive");
820 HashMap::new()
821 }
822 }
823 } else if let Some(legacy) = legacy_manifest.as_ref() {
824 match load_legacy_components_from_archive(&engine, archive_path, legacy) {
825 Ok(map) => map,
826 Err(err) => {
827 warn!(error = %err, pack = %archive_path.display(), "failed to load components from archive");
828 HashMap::new()
829 }
830 }
831 } else {
832 HashMap::new()
833 }
834 } else if is_component {
835 let name = safe_path
836 .file_stem()
837 .map(|s| s.to_string_lossy().to_string())
838 .unwrap_or_else(|| "component".to_string());
839 let component = Component::from_binary(&engine, &wasm_bytes)?;
840 let mut map = HashMap::new();
841 map.insert(
842 name.clone(),
843 PackComponent {
844 name,
845 version: metadata.version.clone(),
846 component,
847 },
848 );
849 map
850 } else {
851 HashMap::new()
852 };
853 let http_client = Arc::clone(&HTTP_CLIENT);
854 Ok(Self {
855 path: safe_path,
856 archive_path: archive_hint.map(Path::to_path_buf),
857 config,
858 engine,
859 metadata,
860 manifest,
861 legacy_manifest,
862 mocks,
863 flows,
864 components,
865 http_client,
866 pre_cache: Mutex::new(HashMap::new()),
867 session_store,
868 state_store,
869 wasi_policy,
870 secrets,
871 oauth_config,
872 })
873 }
874
875 pub async fn list_flows(&self) -> Result<Vec<FlowDescriptor>> {
876 if let Some(cache) = &self.flows {
877 return Ok(cache.descriptors.clone());
878 }
879 if let Some(manifest) = &self.manifest {
880 let descriptors = manifest
881 .flows
882 .iter()
883 .map(|flow| FlowDescriptor {
884 id: flow.id.as_str().to_string(),
885 flow_type: flow_kind_to_str(flow.kind).to_string(),
886 profile: manifest.pack_id.as_str().to_string(),
887 version: manifest.version.to_string(),
888 description: None,
889 })
890 .collect();
891 return Ok(descriptors);
892 }
893 Ok(Vec::new())
894 }
895
896 #[allow(dead_code)]
897 pub async fn run_flow(
898 &self,
899 flow_id: &str,
900 input: serde_json::Value,
901 ) -> Result<serde_json::Value> {
902 let pack = Arc::new(
903 PackRuntime::load(
904 &self.path,
905 Arc::clone(&self.config),
906 self.mocks.clone(),
907 self.archive_path.as_deref(),
908 self.session_store.clone(),
909 self.state_store.clone(),
910 Arc::clone(&self.wasi_policy),
911 self.secrets.clone(),
912 self.oauth_config.clone(),
913 false,
914 )
915 .await?,
916 );
917
918 let engine = FlowEngine::new(vec![Arc::clone(&pack)], Arc::clone(&self.config)).await?;
919 let retry_config = self.config.retry_config().into();
920 let mocks = pack.mocks.as_deref();
921 let tenant = self.config.tenant.as_str();
922
923 let ctx = FlowContext {
924 tenant,
925 flow_id,
926 node_id: None,
927 tool: None,
928 action: None,
929 session_id: None,
930 provider_id: None,
931 retry_config,
932 observer: None,
933 mocks,
934 };
935
936 let execution = engine.execute(ctx, input).await?;
937 match execution.status {
938 FlowStatus::Completed => Ok(execution.output),
939 FlowStatus::Waiting(wait) => Ok(serde_json::json!({
940 "status": "pending",
941 "reason": wait.reason,
942 "resume": wait.snapshot,
943 "response": execution.output,
944 })),
945 }
946 }
947
948 pub async fn invoke_component(
949 &self,
950 component_ref: &str,
951 ctx: ComponentExecCtx,
952 operation: &str,
953 _config_json: Option<String>,
954 input_json: String,
955 ) -> Result<Value> {
956 let pack_component = self
957 .components
958 .get(component_ref)
959 .with_context(|| format!("component '{component_ref}' not found in pack"))?;
960
961 let pre = if let Some(pre) = self.pre_cache.lock().get(component_ref).cloned() {
962 pre
963 } else {
964 let mut linker = Linker::new(&self.engine);
965 register_all(&mut linker)?;
966 add_component_control_to_linker(&mut linker)?;
967 let pre = ComponentPre::new(
968 linker
969 .instantiate_pre(&pack_component.component)
970 .map_err(|err| anyhow!(err))?,
971 )
972 .map_err(|err| anyhow!(err))?;
973 self.pre_cache
974 .lock()
975 .insert(component_ref.to_string(), pre.clone());
976 pre
977 };
978
979 let host_state = HostState::new(
980 Arc::clone(&self.config),
981 Arc::clone(&self.http_client),
982 self.mocks.clone(),
983 self.session_store.clone(),
984 self.state_store.clone(),
985 Arc::clone(&self.secrets),
986 self.oauth_config.clone(),
987 )?;
988 let store_state = ComponentState::new(host_state, Arc::clone(&self.wasi_policy))?;
989 let mut store = wasmtime::Store::new(&self.engine, store_state);
990 let bindings = pre
991 .instantiate_async(&mut store)
992 .await
993 .map_err(|err| anyhow!(err))?;
994 let node = bindings.greentic_component_node();
995
996 let result = node.call_invoke(&mut store, &ctx, operation, &input_json)?;
997
998 match result {
999 InvokeResult::Ok(body) => {
1000 if body.is_empty() {
1001 return Ok(Value::Null);
1002 }
1003 serde_json::from_str(&body).or_else(|_| Ok(Value::String(body)))
1004 }
1005 InvokeResult::Err(NodeError {
1006 code,
1007 message,
1008 retryable,
1009 backoff_ms,
1010 details,
1011 }) => {
1012 let mut obj = serde_json::Map::new();
1013 obj.insert("ok".into(), Value::Bool(false));
1014 let mut error = serde_json::Map::new();
1015 error.insert("code".into(), Value::String(code));
1016 error.insert("message".into(), Value::String(message));
1017 error.insert("retryable".into(), Value::Bool(retryable));
1018 if let Some(backoff) = backoff_ms {
1019 error.insert("backoff_ms".into(), Value::Number(backoff.into()));
1020 }
1021 if let Some(details) = details {
1022 error.insert(
1023 "details".into(),
1024 serde_json::from_str(&details).unwrap_or(Value::String(details)),
1025 );
1026 }
1027 obj.insert("error".into(), Value::Object(error));
1028 Ok(Value::Object(obj))
1029 }
1030 }
1031 }
1032
1033 pub fn load_flow(&self, flow_id: &str) -> Result<Flow> {
1034 if let Some(cache) = &self.flows {
1035 return cache
1036 .flows
1037 .get(flow_id)
1038 .cloned()
1039 .ok_or_else(|| anyhow!("flow '{flow_id}' not found in pack"));
1040 }
1041 if let Some(manifest) = &self.manifest {
1042 let entry = manifest
1043 .flows
1044 .iter()
1045 .find(|f| f.id.as_str() == flow_id)
1046 .ok_or_else(|| anyhow!("flow '{flow_id}' not found in manifest"))?;
1047 return Ok(entry.flow.clone());
1048 }
1049 bail!("flow '{flow_id}' not available (pack exports disabled)")
1050 }
1051
1052 pub fn metadata(&self) -> &PackMetadata {
1053 &self.metadata
1054 }
1055
1056 pub fn required_secrets(&self) -> &[greentic_types::SecretRequirement] {
1057 &self.metadata.secret_requirements
1058 }
1059
1060 pub fn missing_secrets(
1061 &self,
1062 tenant_ctx: &TypesTenantCtx,
1063 ) -> Vec<greentic_types::SecretRequirement> {
1064 let env = tenant_ctx.env.as_str().to_string();
1065 let tenant = tenant_ctx.tenant.as_str().to_string();
1066 let team = tenant_ctx.team.as_ref().map(|t| t.as_str().to_string());
1067 self.required_secrets()
1068 .iter()
1069 .filter(|req| {
1070 if let Some(scope) = &req.scope {
1072 if scope.env != env {
1073 return false;
1074 }
1075 if scope.tenant != tenant {
1076 return false;
1077 }
1078 if let Some(ref team_req) = scope.team
1079 && team.as_ref() != Some(team_req)
1080 {
1081 return false;
1082 }
1083 }
1084 read_secret_blocking(&self.secrets, req.key.as_str()).is_err()
1085 })
1086 .cloned()
1087 .collect()
1088 }
1089
1090 pub fn for_component_test(
1091 components: Vec<(String, PathBuf)>,
1092 flows: HashMap<String, FlowIR>,
1093 config: Arc<HostConfig>,
1094 ) -> Result<Self> {
1095 let engine = Engine::default();
1096 let mut component_map = HashMap::new();
1097 for (name, path) in components {
1098 if !path.exists() {
1099 bail!("component artifact missing: {}", path.display());
1100 }
1101 let wasm_bytes = std::fs::read(&path)?;
1102 let component = Component::from_binary(&engine, &wasm_bytes)
1103 .with_context(|| format!("failed to compile component {}", path.display()))?;
1104 component_map.insert(
1105 name.clone(),
1106 PackComponent {
1107 name,
1108 version: "0.0.0".into(),
1109 component,
1110 },
1111 );
1112 }
1113
1114 let mut flow_map = HashMap::new();
1115 let mut descriptors = Vec::new();
1116 for (id, ir) in flows {
1117 let flow_type = ir.flow_type.clone();
1118 let flow = flow_ir_to_flow(ir)?;
1119 flow_map.insert(id.clone(), flow);
1120 descriptors.push(FlowDescriptor {
1121 id: id.clone(),
1122 flow_type,
1123 profile: "test".into(),
1124 version: "0.0.0".into(),
1125 description: None,
1126 });
1127 }
1128 let flows_cache = PackFlows {
1129 descriptors: descriptors.clone(),
1130 flows: flow_map,
1131 metadata: PackMetadata::fallback(Path::new("component-test")),
1132 };
1133
1134 Ok(Self {
1135 path: PathBuf::new(),
1136 archive_path: None,
1137 config,
1138 engine,
1139 metadata: PackMetadata::fallback(Path::new("component-test")),
1140 manifest: None,
1141 legacy_manifest: None,
1142 mocks: None,
1143 flows: Some(flows_cache),
1144 components: component_map,
1145 http_client: Arc::clone(&HTTP_CLIENT),
1146 pre_cache: Mutex::new(HashMap::new()),
1147 session_store: None,
1148 state_store: None,
1149 wasi_policy: Arc::new(RunnerWasiPolicy::new()),
1150 secrets: crate::secrets::default_manager(),
1151 oauth_config: None,
1152 })
1153 }
1154}
1155
1156struct PackFlows {
1157 descriptors: Vec<FlowDescriptor>,
1158 flows: HashMap<String, Flow>,
1159 metadata: PackMetadata,
1160}
1161
1162impl PackFlows {
1163 fn from_manifest(manifest: greentic_types::PackManifest) -> Self {
1164 let descriptors = manifest
1165 .flows
1166 .iter()
1167 .map(|entry| FlowDescriptor {
1168 id: entry.id.as_str().to_string(),
1169 flow_type: flow_kind_to_str(entry.kind).to_string(),
1170 profile: manifest.pack_id.as_str().to_string(),
1171 version: manifest.version.to_string(),
1172 description: None,
1173 })
1174 .collect();
1175 let mut flows = HashMap::new();
1176 for entry in &manifest.flows {
1177 flows.insert(entry.id.as_str().to_string(), entry.flow.clone());
1178 }
1179 Self {
1180 metadata: PackMetadata::from_manifest(&manifest),
1181 descriptors,
1182 flows,
1183 }
1184 }
1185}
1186
1187fn flow_kind_to_str(kind: greentic_types::FlowKind) -> &'static str {
1188 match kind {
1189 greentic_types::FlowKind::Messaging => "messaging",
1190 greentic_types::FlowKind::Event => "event",
1191 greentic_types::FlowKind::ComponentConfig => "component-config",
1192 greentic_types::FlowKind::Job => "job",
1193 greentic_types::FlowKind::Http => "http",
1194 }
1195}
1196
1197fn read_entry(archive: &mut ZipArchive<File>, name: &str) -> Result<Vec<u8>> {
1198 let mut file = archive
1199 .by_name(name)
1200 .with_context(|| format!("entry {name} missing from archive"))?;
1201 let mut buf = Vec::new();
1202 file.read_to_end(&mut buf)?;
1203 Ok(buf)
1204}
1205
1206fn normalize_flow_doc(mut doc: FlowDoc) -> FlowDoc {
1207 for node in doc.nodes.values_mut() {
1208 if node.component.is_empty()
1209 && let Some((component_ref, payload)) = node.raw.iter().next()
1210 {
1211 if component_ref.starts_with("emit.") {
1212 node.component = component_ref.clone();
1213 node.payload = payload.clone();
1214 node.raw.clear();
1215 continue;
1216 }
1217 let (target_component, operation, input, config) =
1218 infer_component_exec(payload, component_ref);
1219 let mut payload_obj = serde_json::Map::new();
1220 payload_obj.insert("component".into(), Value::String(target_component));
1222 payload_obj.insert("operation".into(), Value::String(operation));
1223 payload_obj.insert("input".into(), input);
1224 if let Some(cfg) = config {
1225 payload_obj.insert("config".into(), cfg);
1226 }
1227 node.component = "component.exec".to_string();
1228 node.payload = Value::Object(payload_obj);
1229 }
1230 }
1231 doc
1232}
1233
1234fn infer_component_exec(
1235 payload: &Value,
1236 component_ref: &str,
1237) -> (String, String, Value, Option<Value>) {
1238 let default_op = if component_ref.starts_with("templating.") {
1239 "render"
1240 } else {
1241 "invoke"
1242 }
1243 .to_string();
1244
1245 if let Value::Object(map) = payload {
1246 let op = map
1247 .get("op")
1248 .or_else(|| map.get("operation"))
1249 .and_then(Value::as_str)
1250 .map(|s| s.to_string())
1251 .unwrap_or_else(|| default_op.clone());
1252
1253 let mut input = map.clone();
1254 let config = input.remove("config");
1255 let component = input
1256 .get("component")
1257 .or_else(|| input.get("component_ref"))
1258 .and_then(Value::as_str)
1259 .map(|s| s.to_string())
1260 .unwrap_or_else(|| component_ref.to_string());
1261 input.remove("component");
1262 input.remove("component_ref");
1263 input.remove("op");
1264 input.remove("operation");
1265 return (component, op, Value::Object(input), config);
1266 }
1267
1268 (component_ref.to_string(), default_op, payload.clone(), None)
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273 use super::*;
1274 use greentic_flow::model::{FlowDoc, NodeDoc};
1275 use serde_json::json;
1276 use std::collections::BTreeMap;
1277
1278 #[test]
1279 fn normalizes_raw_component_to_component_exec() {
1280 let mut nodes = BTreeMap::new();
1281 let mut raw = BTreeMap::new();
1282 raw.insert(
1283 "templating.handlebars".into(),
1284 json!({ "template": "Hi {{name}}" }),
1285 );
1286 nodes.insert(
1287 "start".into(),
1288 NodeDoc {
1289 raw,
1290 routing: json!([{"out": true}]),
1291 ..Default::default()
1292 },
1293 );
1294 let doc = FlowDoc {
1295 id: "welcome".into(),
1296 title: None,
1297 description: None,
1298 flow_type: "messaging".into(),
1299 start: Some("start".into()),
1300 parameters: json!({}),
1301 tags: Vec::new(),
1302 entrypoints: BTreeMap::new(),
1303 nodes,
1304 };
1305
1306 let normalized = normalize_flow_doc(doc);
1307 let node = normalized.nodes.get("start").expect("node exists");
1308 assert_eq!(node.component, "component.exec");
1309 assert!(node.raw.is_empty() || node.raw.contains_key("templating.handlebars"));
1310 let payload = node.payload.as_object().expect("payload object");
1311 assert_eq!(
1312 payload.get("component"),
1313 Some(&Value::String("templating.handlebars".into()))
1314 );
1315 assert_eq!(
1316 payload.get("operation"),
1317 Some(&Value::String("render".into()))
1318 );
1319 let input = payload.get("input").unwrap();
1320 assert_eq!(input, &json!({ "template": "Hi {{name}}" }));
1321 }
1322}
1323
1324fn load_components_from_archive(
1325 engine: &Engine,
1326 path: &Path,
1327 manifest: Option<&greentic_types::PackManifest>,
1328) -> Result<HashMap<String, PackComponent>> {
1329 let mut archive = ZipArchive::new(File::open(path)?)
1330 .with_context(|| format!("{} is not a valid gtpack", path.display()))?;
1331 let mut components = HashMap::new();
1332 if let Some(manifest) = manifest {
1333 for entry in &manifest.components {
1334 let file_name = format!("components/{}.wasm", entry.id.as_str());
1335 let bytes = read_entry(&mut archive, &file_name)
1336 .with_context(|| format!("missing component {}", file_name))?;
1337 let component = Component::from_binary(engine, &bytes)
1338 .with_context(|| format!("failed to compile component {}", entry.id.as_str()))?;
1339 components.insert(
1340 entry.id.as_str().to_string(),
1341 PackComponent {
1342 name: entry.id.as_str().to_string(),
1343 version: entry.version.to_string(),
1344 component,
1345 },
1346 );
1347 }
1348 }
1349 Ok(components)
1350}
1351
1352fn load_legacy_components_from_archive(
1353 engine: &Engine,
1354 path: &Path,
1355 manifest: &legacy_pack::PackManifest,
1356) -> Result<HashMap<String, PackComponent>> {
1357 let mut archive = ZipArchive::new(File::open(path)?)
1358 .with_context(|| format!("{} is not a valid gtpack", path.display()))?;
1359 let mut components = HashMap::new();
1360 for entry in &manifest.components {
1361 let bytes = read_entry(&mut archive, &entry.file_wasm)
1362 .with_context(|| format!("missing component {}", entry.file_wasm))?;
1363 let component = Component::from_binary(engine, &bytes)
1364 .with_context(|| format!("failed to compile component {}", entry.name))?;
1365 components.insert(
1366 entry.name.clone(),
1367 PackComponent {
1368 name: entry.name.clone(),
1369 version: entry.version.to_string(),
1370 component,
1371 },
1372 );
1373 }
1374 Ok(components)
1375}
1376
1377#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1378pub struct PackMetadata {
1379 pub pack_id: String,
1380 pub version: String,
1381 #[serde(default)]
1382 pub entry_flows: Vec<String>,
1383 #[serde(default)]
1384 pub secret_requirements: Vec<greentic_types::SecretRequirement>,
1385}
1386
1387impl PackMetadata {
1388 fn from_wasm(bytes: &[u8]) -> Option<Self> {
1389 let parser = Parser::new(0);
1390 for payload in parser.parse_all(bytes) {
1391 let payload = payload.ok()?;
1392 match payload {
1393 Payload::CustomSection(section) => {
1394 if section.name() == "greentic.manifest"
1395 && let Ok(meta) = Self::from_bytes(section.data())
1396 {
1397 return Some(meta);
1398 }
1399 }
1400 Payload::DataSection(reader) => {
1401 for segment in reader.into_iter().flatten() {
1402 if let Ok(meta) = Self::from_bytes(segment.data) {
1403 return Some(meta);
1404 }
1405 }
1406 }
1407 _ => {}
1408 }
1409 }
1410 None
1411 }
1412
1413 fn from_bytes(bytes: &[u8]) -> Result<Self, serde_cbor::Error> {
1414 #[derive(Deserialize)]
1415 struct RawManifest {
1416 pack_id: String,
1417 version: String,
1418 #[serde(default)]
1419 entry_flows: Vec<String>,
1420 #[serde(default)]
1421 flows: Vec<RawFlow>,
1422 #[serde(default)]
1423 secret_requirements: Vec<greentic_types::SecretRequirement>,
1424 }
1425
1426 #[derive(Deserialize)]
1427 struct RawFlow {
1428 id: String,
1429 }
1430
1431 let manifest: RawManifest = serde_cbor::from_slice(bytes)?;
1432 let mut entry_flows = if manifest.entry_flows.is_empty() {
1433 manifest.flows.iter().map(|f| f.id.clone()).collect()
1434 } else {
1435 manifest.entry_flows.clone()
1436 };
1437 entry_flows.retain(|id| !id.is_empty());
1438 Ok(Self {
1439 pack_id: manifest.pack_id,
1440 version: manifest.version,
1441 entry_flows,
1442 secret_requirements: manifest.secret_requirements,
1443 })
1444 }
1445
1446 pub fn fallback(path: &Path) -> Self {
1447 let pack_id = path
1448 .file_stem()
1449 .map(|s| s.to_string_lossy().into_owned())
1450 .unwrap_or_else(|| "unknown-pack".to_string());
1451 Self {
1452 pack_id,
1453 version: "0.0.0".to_string(),
1454 entry_flows: Vec::new(),
1455 secret_requirements: Vec::new(),
1456 }
1457 }
1458
1459 pub fn from_manifest(manifest: &greentic_types::PackManifest) -> Self {
1460 let entry_flows = manifest
1461 .flows
1462 .iter()
1463 .map(|flow| flow.id.as_str().to_string())
1464 .collect::<Vec<_>>();
1465 Self {
1466 pack_id: manifest.pack_id.as_str().to_string(),
1467 version: manifest.version.to_string(),
1468 entry_flows,
1469 secret_requirements: manifest.secret_requirements.clone(),
1470 }
1471 }
1472}