Skip to main content

greentic_types/
pack_manifest.rs

1//! Canonical pack manifest (.gtpack) representation embedding flows and components.
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use semver::Version;
8
9use crate::pack::extensions::capabilities::{
10    CapabilitiesExtensionError, CapabilitiesExtensionV1, EXT_CAPABILITIES_V1,
11};
12use crate::pack::extensions::component_sources::{
13    ComponentSourcesError, ComponentSourcesV1, EXT_COMPONENT_SOURCES_V1,
14};
15use crate::{
16    ComponentManifest, Flow, FlowId, FlowKind, PROVIDER_EXTENSION_ID, PackId,
17    ProviderExtensionInline, SecretRequirement, SemverReq, Signature,
18};
19
20#[cfg(feature = "schemars")]
21use schemars::JsonSchema;
22#[cfg(feature = "serde")]
23use serde::{Deserialize, Serialize};
24
25#[cfg(feature = "schemars")]
26fn empty_secret_requirements() -> Vec<SecretRequirement> {
27    Vec::new()
28}
29
30pub(crate) fn extensions_is_empty(value: &Option<BTreeMap<String, ExtensionRef>>) -> bool {
31    value.as_ref().is_none_or(BTreeMap::is_empty)
32}
33
34/// Hint describing the primary purpose of a pack.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
37#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
38#[cfg_attr(feature = "schemars", derive(JsonSchema))]
39pub enum PackKind {
40    /// Application packs.
41    Application,
42    /// Provider packs exporting components.
43    Provider,
44    /// Infrastructure packs.
45    Infrastructure,
46    /// Library packs.
47    Library,
48}
49
50/// Pack manifest describing bundled flows and components.
51#[derive(Clone, Debug, PartialEq)]
52#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
53#[cfg_attr(
54    feature = "schemars",
55    derive(JsonSchema),
56    schemars(
57        title = "Greentic PackManifest v1",
58        description = "Canonical pack manifest embedding flows, components, dependencies and signatures.",
59        rename = "greentic.pack-manifest.v1"
60    )
61)]
62pub struct PackManifest {
63    /// Schema version for the pack manifest.
64    pub schema_version: String,
65    /// Logical pack identifier.
66    pub pack_id: PackId,
67    /// Optional human-readable name from `pack.yaml`.
68    #[cfg_attr(
69        feature = "schemars",
70        schemars(default, description = "Optional pack name")
71    )]
72    #[cfg_attr(
73        feature = "serde",
74        serde(default, skip_serializing_if = "Option::is_none")
75    )]
76    pub name: Option<String>,
77    /// Pack semantic version.
78    #[cfg_attr(
79        feature = "schemars",
80        schemars(with = "String", description = "SemVer version")
81    )]
82    pub version: Version,
83    /// Pack kind hint.
84    pub kind: PackKind,
85    /// Pack publisher.
86    pub publisher: String,
87    /// Component descriptors bundled within the pack.
88    #[cfg_attr(feature = "serde", serde(default))]
89    pub components: Vec<ComponentManifest>,
90    /// Flow entries embedded in the pack.
91    #[cfg_attr(feature = "serde", serde(default))]
92    pub flows: Vec<PackFlowEntry>,
93    /// Pack dependencies.
94    #[cfg_attr(feature = "serde", serde(default))]
95    pub dependencies: Vec<PackDependency>,
96    /// Capability declarations for the pack.
97    #[cfg_attr(feature = "serde", serde(default))]
98    pub capabilities: Vec<ComponentCapability>,
99    /// Pack-level secret requirements.
100    #[cfg_attr(
101        feature = "serde",
102        serde(default, skip_serializing_if = "Vec::is_empty")
103    )]
104    #[cfg_attr(feature = "schemars", schemars(default = "empty_secret_requirements"))]
105    pub secret_requirements: Vec<SecretRequirement>,
106    /// Pack signatures.
107    #[cfg_attr(feature = "serde", serde(default))]
108    pub signatures: PackSignatures,
109    /// Optional bootstrap/install hints for platform-controlled packs.
110    #[cfg_attr(
111        feature = "serde",
112        serde(default, skip_serializing_if = "Option::is_none")
113    )]
114    pub bootstrap: Option<BootstrapSpec>,
115    /// Optional extension descriptors for provider-specific metadata.
116    #[cfg_attr(
117        feature = "serde",
118        serde(default, skip_serializing_if = "extensions_is_empty")
119    )]
120    pub extensions: Option<BTreeMap<String, ExtensionRef>>,
121    /// Per-agent config blobs (opaque, self-describing) for Agentic Worker
122    /// (`dw.agent`) flow nodes, keyed by `agent_id`. The runtime deserialises
123    /// each blob into its own agent config type; this crate stays agnostic.
124    #[cfg_attr(
125        feature = "serde",
126        serde(default, skip_serializing_if = "BTreeMap::is_empty")
127    )]
128    #[cfg_attr(feature = "schemars", schemars(default))]
129    pub agents: BTreeMap<String, serde_json::Value>,
130}
131
132/// Flow entry embedded in a pack.
133#[derive(Clone, Debug, PartialEq)]
134#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
135#[cfg_attr(feature = "schemars", derive(JsonSchema))]
136pub struct PackFlowEntry {
137    /// Flow identifier.
138    pub id: FlowId,
139    /// Flow kind.
140    pub kind: FlowKind,
141    /// Flow definition.
142    pub flow: Flow,
143    /// Flow tags.
144    #[cfg_attr(feature = "serde", serde(default))]
145    pub tags: Vec<String>,
146    /// Additional entrypoint identifiers for discoverability.
147    #[cfg_attr(feature = "serde", serde(default))]
148    pub entrypoints: Vec<String>,
149}
150
151/// Dependency entry referencing another pack.
152#[derive(Clone, Debug, PartialEq, Eq)]
153#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
154#[cfg_attr(feature = "schemars", derive(JsonSchema))]
155pub struct PackDependency {
156    /// Local alias for the dependency.
157    pub alias: String,
158    /// Referenced pack identifier.
159    pub pack_id: PackId,
160    /// Required version.
161    pub version_req: SemverReq,
162    /// Required capabilities.
163    #[cfg_attr(feature = "serde", serde(default))]
164    pub required_capabilities: Vec<String>,
165}
166
167/// Named capability advertised by a pack or component collection.
168#[derive(Clone, Debug, PartialEq, Eq)]
169#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
170#[cfg_attr(feature = "schemars", derive(JsonSchema))]
171pub struct ComponentCapability {
172    /// Capability name.
173    pub name: String,
174    /// Optional description or metadata.
175    #[cfg_attr(
176        feature = "serde",
177        serde(default, skip_serializing_if = "Option::is_none")
178    )]
179    pub description: Option<String>,
180}
181
182/// Signature bundle accompanying a pack manifest.
183#[derive(Clone, Debug, PartialEq, Eq, Default)]
184#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
185#[cfg_attr(feature = "schemars", derive(JsonSchema))]
186pub struct PackSignatures {
187    /// Optional detached signatures.
188    #[cfg_attr(feature = "serde", serde(default))]
189    pub signatures: Vec<Signature>,
190}
191
192/// Optional bootstrap/install hints for platform-managed packs.
193#[derive(Clone, Debug, PartialEq, Eq, Default)]
194#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
195#[cfg_attr(feature = "schemars", derive(JsonSchema))]
196pub struct BootstrapSpec {
197    /// Flow to run during initial install/bootstrap.
198    #[cfg_attr(
199        feature = "serde",
200        serde(default, skip_serializing_if = "Option::is_none")
201    )]
202    pub install_flow: Option<String>,
203    /// Flow to run when upgrading an existing install.
204    #[cfg_attr(
205        feature = "serde",
206        serde(default, skip_serializing_if = "Option::is_none")
207    )]
208    pub upgrade_flow: Option<String>,
209    /// Component responsible for install/upgrade orchestration.
210    #[cfg_attr(
211        feature = "serde",
212        serde(default, skip_serializing_if = "Option::is_none")
213    )]
214    pub installer_component: Option<String>,
215}
216
217/// Inline payload for a pack extension entry.
218#[derive(Clone, Debug, PartialEq)]
219#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
220#[cfg_attr(feature = "serde", serde(untagged))]
221#[cfg_attr(feature = "schemars", derive(JsonSchema))]
222pub enum ExtensionInline {
223    /// Provider extension payload embedding provider declarations.
224    Provider(ProviderExtensionInline),
225    /// Arbitrary inline payload for unknown extensions.
226    Other(serde_json::Value),
227}
228
229impl ExtensionInline {
230    /// Returns the provider inline payload if present.
231    pub fn as_provider_inline(&self) -> Option<&ProviderExtensionInline> {
232        match self {
233            ExtensionInline::Provider(value) => Some(value),
234            ExtensionInline::Other(_) => None,
235        }
236    }
237
238    /// Returns a mutable provider inline payload if present.
239    pub fn as_provider_inline_mut(&mut self) -> Option<&mut ProviderExtensionInline> {
240        match self {
241            ExtensionInline::Provider(value) => Some(value),
242            ExtensionInline::Other(_) => None,
243        }
244    }
245}
246
247/// External extension reference embedded in a pack manifest.
248#[derive(Clone, Debug, PartialEq)]
249#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
250#[cfg_attr(feature = "schemars", derive(JsonSchema))]
251pub struct ExtensionRef {
252    /// Extension kind identifier, e.g. `greentic.provider-extension.v1` (only this ID is supported for provider metadata; other keys are treated as unknown extensions).
253    pub kind: String,
254    /// Extension version as a string to avoid semver crate coupling.
255    pub version: String,
256    /// Optional digest pin for the referenced extension payload.
257    #[cfg_attr(
258        feature = "serde",
259        serde(default, skip_serializing_if = "Option::is_none")
260    )]
261    pub digest: Option<String>,
262    /// Optional remote or local location for the extension payload.
263    #[cfg_attr(
264        feature = "serde",
265        serde(default, skip_serializing_if = "Option::is_none")
266    )]
267    pub location: Option<String>,
268    /// Optional inline extension payload for small metadata blobs.
269    #[cfg_attr(
270        feature = "serde",
271        serde(default, skip_serializing_if = "Option::is_none")
272    )]
273    pub inline: Option<ExtensionInline>,
274}
275
276impl PackManifest {
277    /// Returns the inline provider extension payload if present.
278    pub fn provider_extension_inline(&self) -> Option<&ProviderExtensionInline> {
279        self.extensions
280            .as_ref()
281            .and_then(|extensions| extensions.get(PROVIDER_EXTENSION_ID))
282            .and_then(|extension| extension.inline.as_ref())
283            .and_then(ExtensionInline::as_provider_inline)
284    }
285
286    /// Returns a mutable inline provider extension payload if present.
287    pub fn provider_extension_inline_mut(&mut self) -> Option<&mut ProviderExtensionInline> {
288        self.extensions
289            .as_mut()
290            .and_then(|extensions| extensions.get_mut(PROVIDER_EXTENSION_ID))
291            .and_then(|extension| extension.inline.as_mut())
292            .map(|inline| {
293                if let ExtensionInline::Other(value) = inline {
294                    let parsed = serde_json::from_value(value.clone())
295                        .unwrap_or_else(|_| ProviderExtensionInline::default());
296                    *inline = ExtensionInline::Provider(parsed);
297                }
298                inline
299            })
300            .and_then(ExtensionInline::as_provider_inline_mut)
301    }
302
303    /// Ensures the provider extension entry exists and returns its inline payload.
304    pub fn ensure_provider_extension_inline(&mut self) -> &mut ProviderExtensionInline {
305        let extensions = self.extensions.get_or_insert_with(BTreeMap::new);
306        let entry = extensions
307            .entry(PROVIDER_EXTENSION_ID.to_string())
308            .or_insert_with(|| ExtensionRef {
309                kind: PROVIDER_EXTENSION_ID.to_string(),
310                version: "1.0.0".to_string(),
311                digest: None,
312                location: None,
313                inline: Some(ExtensionInline::Provider(ProviderExtensionInline::default())),
314            });
315        if entry.inline.is_none() {
316            entry.inline = Some(ExtensionInline::Provider(ProviderExtensionInline::default()));
317        }
318        let inline = entry
319            .inline
320            .get_or_insert_with(|| ExtensionInline::Provider(ProviderExtensionInline::default()));
321        if let ExtensionInline::Other(value) = inline {
322            let parsed = serde_json::from_value(value.clone())
323                .unwrap_or_else(|_| ProviderExtensionInline::default());
324            *inline = ExtensionInline::Provider(parsed);
325        }
326        match inline {
327            ExtensionInline::Provider(inline) => inline,
328            ExtensionInline::Other(_) => unreachable!("provider inline should be initialised"),
329        }
330    }
331
332    /// Returns the component sources extension payload if present.
333    #[cfg(feature = "serde")]
334    pub fn get_component_sources_v1(
335        &self,
336    ) -> Result<Option<ComponentSourcesV1>, ComponentSourcesError> {
337        let extension = self
338            .extensions
339            .as_ref()
340            .and_then(|extensions| extensions.get(EXT_COMPONENT_SOURCES_V1));
341        let inline = match extension.and_then(|entry| entry.inline.as_ref()) {
342            Some(ExtensionInline::Other(value)) => value,
343            Some(_) => return Err(ComponentSourcesError::UnexpectedInline),
344            None => return Ok(None),
345        };
346        let payload = ComponentSourcesV1::from_extension_value(inline)?;
347        Ok(Some(payload))
348    }
349
350    /// Sets the component sources extension payload.
351    #[cfg(feature = "serde")]
352    pub fn set_component_sources_v1(
353        &mut self,
354        sources: ComponentSourcesV1,
355    ) -> Result<(), ComponentSourcesError> {
356        sources.validate_schema_version()?;
357        let inline = sources.to_extension_value()?;
358        let extensions = self.extensions.get_or_insert_with(BTreeMap::new);
359        extensions.insert(
360            EXT_COMPONENT_SOURCES_V1.to_string(),
361            ExtensionRef {
362                kind: EXT_COMPONENT_SOURCES_V1.to_string(),
363                version: "1.0.0".to_string(),
364                digest: None,
365                location: None,
366                inline: Some(ExtensionInline::Other(inline)),
367            },
368        );
369        Ok(())
370    }
371
372    /// Returns the capabilities extension payload if present.
373    #[cfg(feature = "serde")]
374    pub fn get_capabilities_extension_v1(
375        &self,
376    ) -> Result<Option<CapabilitiesExtensionV1>, CapabilitiesExtensionError> {
377        let extension = self
378            .extensions
379            .as_ref()
380            .and_then(|extensions| extensions.get(EXT_CAPABILITIES_V1));
381        let inline = match extension.and_then(|entry| entry.inline.as_ref()) {
382            Some(ExtensionInline::Other(value)) => value,
383            Some(_) => return Err(CapabilitiesExtensionError::UnexpectedInline),
384            None => return Ok(None),
385        };
386        let payload = CapabilitiesExtensionV1::from_extension_value(inline)?;
387        Ok(Some(payload))
388    }
389
390    /// Sets the capabilities extension payload.
391    #[cfg(feature = "serde")]
392    pub fn set_capabilities_extension_v1(
393        &mut self,
394        capabilities: CapabilitiesExtensionV1,
395    ) -> Result<(), CapabilitiesExtensionError> {
396        capabilities.validate()?;
397        let inline = capabilities.to_extension_value()?;
398        let extensions = self.extensions.get_or_insert_with(BTreeMap::new);
399        extensions.insert(
400            EXT_CAPABILITIES_V1.to_string(),
401            ExtensionRef {
402                kind: EXT_CAPABILITIES_V1.to_string(),
403                version: "1.0.0".to_string(),
404                digest: None,
405                location: None,
406                inline: Some(ExtensionInline::Other(inline)),
407            },
408        );
409        Ok(())
410    }
411}