greentic_runner_host/
pack.rs

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    /// Component artifact path (wasm file).
74    path: PathBuf,
75    /// Optional archive (.gtpack) used to load flows/manifests.
76    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            // Fall back to legacy pack manifest
589            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        // no-op cooperative yield
674    }
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                // scope must match current context if provided
1071                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            // component.exec is meta; ensure the payload carries the actual target component.
1221            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}