greentic_types/
component.rs

1//! Component manifest structures with generic capability declarations.
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use semver::Version;
8
9use crate::flow::FlowKind;
10use crate::{ComponentId, FlowId, SecretRequirement};
11
12#[cfg(feature = "schemars")]
13use schemars::JsonSchema;
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16
17/// Development-time flow embedded directly in a component manifest.
18///
19/// These flows are consumed by tooling such as `greentic-dev` during authoring. They are not
20/// required for deployment or runtime execution and may be safely ignored by hosts and runners.
21#[derive(Clone, Debug, PartialEq)]
22#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23#[cfg_attr(feature = "schemars", derive(JsonSchema))]
24pub struct ComponentDevFlow {
25    /// Flow representation format. Currently only `flow-ir-json` is supported.
26    #[cfg_attr(feature = "serde", serde(default = "dev_flow_default_format"))]
27    pub format: String,
28    /// FlowIR JSON graph for this flow.
29    pub graph: serde_json::Value,
30}
31
32fn dev_flow_default_format() -> String {
33    "flow-ir-json".to_owned()
34}
35
36/// Component metadata describing capabilities and supported flows.
37#[derive(Clone, Debug, PartialEq)]
38#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
39#[cfg_attr(feature = "schemars", derive(JsonSchema))]
40pub struct ComponentManifest {
41    /// Logical component identifier (opaque string).
42    pub id: ComponentId,
43    /// Semantic component version.
44    #[cfg_attr(
45        feature = "schemars",
46        schemars(with = "String", description = "SemVer version")
47    )]
48    pub version: Version,
49    /// Flow kinds this component can participate in.
50    #[cfg_attr(feature = "serde", serde(default))]
51    pub supports: Vec<FlowKind>,
52    /// Referenced WIT world binding.
53    pub world: String,
54    /// Profile metadata for the component.
55    pub profiles: ComponentProfiles,
56    /// Capability contract required by the component.
57    pub capabilities: ComponentCapabilities,
58    /// Optional configurator flows.
59    #[cfg_attr(
60        feature = "serde",
61        serde(default, skip_serializing_if = "Option::is_none")
62    )]
63    pub configurators: Option<ComponentConfigurators>,
64    /// Operation-level descriptions.
65    #[cfg_attr(feature = "serde", serde(default))]
66    pub operations: Vec<ComponentOperation>,
67    /// Optional configuration schema.
68    #[cfg_attr(
69        feature = "serde",
70        serde(default, skip_serializing_if = "Option::is_none")
71    )]
72    pub config_schema: Option<serde_json::Value>,
73    /// Resource usage hints for deployers/schedulers.
74    #[cfg_attr(feature = "serde", serde(default))]
75    pub resources: ResourceHints,
76    /// Development-time flows used for authoring only. This field is optional and ignored by
77    /// runtime systems. Tools may store FlowIR-as-JSON values here to allow editing flows without
78    /// sidecar files.
79    #[cfg_attr(
80        feature = "serde",
81        serde(default, skip_serializing_if = "BTreeMap::is_empty")
82    )]
83    pub dev_flows: BTreeMap<FlowId, ComponentDevFlow>,
84}
85
86impl ComponentManifest {
87    /// Returns `true` when the component supports the specified flow kind.
88    pub fn supports_kind(&self, kind: FlowKind) -> bool {
89        self.supports.iter().copied().any(|entry| entry == kind)
90    }
91
92    /// Resolves the effective profile name, returning the requested profile when supported or
93    /// falling back to the manifest default.
94    pub fn select_profile<'a>(
95        &'a self,
96        requested: Option<&str>,
97    ) -> Result<Option<&'a str>, ComponentProfileError> {
98        if let Some(name) = requested {
99            let matched = self
100                .profiles
101                .supported
102                .iter()
103                .find(|candidate| candidate.as_str() == name)
104                .ok_or_else(|| ComponentProfileError::UnsupportedProfile {
105                    requested: name.to_owned(),
106                    supported: self.profiles.supported.clone(),
107                })?;
108            Ok(Some(matched.as_str()))
109        } else {
110            Ok(self.profiles.default.as_deref())
111        }
112    }
113
114    /// Returns the optional basic configurator flow identifier.
115    pub fn basic_configurator(&self) -> Option<&FlowId> {
116        self.configurators
117            .as_ref()
118            .and_then(|cfg| cfg.basic.as_ref())
119    }
120
121    /// Returns the optional full configurator flow identifier.
122    pub fn full_configurator(&self) -> Option<&FlowId> {
123        self.configurators
124            .as_ref()
125            .and_then(|cfg| cfg.full.as_ref())
126    }
127}
128
129/// Component profile declaration.
130#[derive(Clone, Debug, PartialEq, Eq, Default)]
131#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
132#[cfg_attr(feature = "schemars", derive(JsonSchema))]
133pub struct ComponentProfiles {
134    /// Default profile applied when a node does not specify one.
135    #[cfg_attr(
136        feature = "serde",
137        serde(default, skip_serializing_if = "Option::is_none")
138    )]
139    pub default: Option<String>,
140    /// Supported profile identifiers.
141    #[cfg_attr(feature = "serde", serde(default))]
142    pub supported: Vec<String>,
143}
144
145/// Flow configurators linked from a component manifest.
146#[derive(Clone, Debug, PartialEq, Eq)]
147#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
148#[cfg_attr(feature = "schemars", derive(JsonSchema))]
149pub struct ComponentConfigurators {
150    /// Basic configurator flow identifier.
151    #[cfg_attr(
152        feature = "serde",
153        serde(default, skip_serializing_if = "Option::is_none")
154    )]
155    pub basic: Option<FlowId>,
156    /// Full configurator flow identifier.
157    #[cfg_attr(
158        feature = "serde",
159        serde(default, skip_serializing_if = "Option::is_none")
160    )]
161    pub full: Option<FlowId>,
162}
163
164/// Operation descriptor for a component.
165#[derive(Clone, Debug, PartialEq)]
166#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
167#[cfg_attr(feature = "schemars", derive(JsonSchema))]
168pub struct ComponentOperation {
169    /// Operation name (for example `handle_message`).
170    pub name: String,
171    /// Input schema for the operation.
172    pub input_schema: serde_json::Value,
173    /// Output schema for the operation.
174    pub output_schema: serde_json::Value,
175}
176
177/// Resource usage hints for a component.
178#[derive(Clone, Debug, PartialEq, Eq, Default)]
179#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
180#[cfg_attr(feature = "schemars", derive(JsonSchema))]
181pub struct ResourceHints {
182    /// Suggested CPU in millis.
183    #[cfg_attr(
184        feature = "serde",
185        serde(default, skip_serializing_if = "Option::is_none")
186    )]
187    pub cpu_millis: Option<u32>,
188    /// Suggested memory in MiB.
189    #[cfg_attr(
190        feature = "serde",
191        serde(default, skip_serializing_if = "Option::is_none")
192    )]
193    pub memory_mb: Option<u32>,
194    /// Expected average latency in milliseconds.
195    #[cfg_attr(
196        feature = "serde",
197        serde(default, skip_serializing_if = "Option::is_none")
198    )]
199    pub average_latency_ms: Option<u32>,
200}
201
202/// Host + WASI capabilities required by a component.
203#[derive(Clone, Debug, PartialEq, Eq, Default)]
204#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
205#[cfg_attr(feature = "schemars", derive(JsonSchema))]
206pub struct ComponentCapabilities {
207    /// WASI Preview 2 surfaces.
208    pub wasi: WasiCapabilities,
209    /// Host capability surfaces.
210    pub host: HostCapabilities,
211}
212
213/// WASI capability declarations.
214#[derive(Clone, Debug, PartialEq, Eq, Default)]
215#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
216#[cfg_attr(feature = "schemars", derive(JsonSchema))]
217pub struct WasiCapabilities {
218    /// Filesystem configuration.
219    #[cfg_attr(
220        feature = "serde",
221        serde(default, skip_serializing_if = "Option::is_none")
222    )]
223    pub filesystem: Option<FilesystemCapabilities>,
224    /// Environment variable allow list.
225    #[cfg_attr(
226        feature = "serde",
227        serde(default, skip_serializing_if = "Option::is_none")
228    )]
229    pub env: Option<EnvCapabilities>,
230    /// Whether random number generation is required.
231    #[cfg_attr(feature = "serde", serde(default))]
232    pub random: bool,
233    /// Whether clock access is required.
234    #[cfg_attr(feature = "serde", serde(default))]
235    pub clocks: bool,
236}
237
238/// Filesystem sandbox configuration.
239#[derive(Clone, Debug, PartialEq, Eq)]
240#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
241#[cfg_attr(feature = "schemars", derive(JsonSchema))]
242pub struct FilesystemCapabilities {
243    /// Filesystem exposure mode.
244    pub mode: FilesystemMode,
245    /// Declared mounts.
246    #[cfg_attr(feature = "serde", serde(default))]
247    pub mounts: Vec<FilesystemMount>,
248}
249
250impl Default for FilesystemCapabilities {
251    fn default() -> Self {
252        Self {
253            mode: FilesystemMode::None,
254            mounts: Vec::new(),
255        }
256    }
257}
258
259/// Filesystem exposure mode.
260#[derive(Clone, Debug, PartialEq, Eq, Default)]
261#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
262#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
263#[cfg_attr(feature = "schemars", derive(JsonSchema))]
264pub enum FilesystemMode {
265    /// No filesystem access.
266    #[default]
267    None,
268    /// Read-only view with predefined mounts.
269    ReadOnly,
270    /// Isolated sandbox with write access.
271    Sandbox,
272}
273
274/// Single mount definition.
275#[derive(Clone, Debug, PartialEq, Eq)]
276#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
277#[cfg_attr(feature = "schemars", derive(JsonSchema))]
278pub struct FilesystemMount {
279    /// Logical mount identifier.
280    pub name: String,
281    /// Host-provided storage class (scratch/cache/config/etc.).
282    pub host_class: String,
283    /// Guest-visible mount path.
284    pub guest_path: String,
285}
286
287/// Environment variable allow list.
288#[derive(Clone, Debug, PartialEq, Eq, Default)]
289#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
290#[cfg_attr(feature = "schemars", derive(JsonSchema))]
291pub struct EnvCapabilities {
292    /// Environment variable names components may read.
293    #[cfg_attr(feature = "serde", serde(default))]
294    pub allow: Vec<String>,
295}
296
297/// Host capability declaration.
298#[derive(Clone, Debug, PartialEq, Eq, Default)]
299#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
300#[cfg_attr(feature = "schemars", derive(JsonSchema))]
301pub struct HostCapabilities {
302    /// Secret resolution requirements.
303    #[cfg_attr(
304        feature = "serde",
305        serde(default, skip_serializing_if = "Option::is_none")
306    )]
307    pub secrets: Option<SecretsCapabilities>,
308    /// Durable state access requirements.
309    #[cfg_attr(
310        feature = "serde",
311        serde(default, skip_serializing_if = "Option::is_none")
312    )]
313    pub state: Option<StateCapabilities>,
314    /// Messaging ingress/egress needs.
315    #[cfg_attr(
316        feature = "serde",
317        serde(default, skip_serializing_if = "Option::is_none")
318    )]
319    pub messaging: Option<MessagingCapabilities>,
320    /// Event ingress/egress needs.
321    #[cfg_attr(
322        feature = "serde",
323        serde(default, skip_serializing_if = "Option::is_none")
324    )]
325    pub events: Option<EventsCapabilities>,
326    /// HTTP client/server needs.
327    #[cfg_attr(
328        feature = "serde",
329        serde(default, skip_serializing_if = "Option::is_none")
330    )]
331    pub http: Option<HttpCapabilities>,
332    /// Telemetry emission settings.
333    #[cfg_attr(
334        feature = "serde",
335        serde(default, skip_serializing_if = "Option::is_none")
336    )]
337    pub telemetry: Option<TelemetryCapabilities>,
338    /// Infrastructure-as-code artifact permissions.
339    #[cfg_attr(
340        feature = "serde",
341        serde(default, skip_serializing_if = "Option::is_none")
342    )]
343    pub iac: Option<IaCCapabilities>,
344}
345
346/// Secret requirements.
347#[derive(Clone, Debug, PartialEq, Eq, Default)]
348#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
349#[cfg_attr(feature = "schemars", derive(JsonSchema))]
350pub struct SecretsCapabilities {
351    /// Secret identifiers required at runtime.
352    #[cfg_attr(feature = "serde", serde(default))]
353    pub required: Vec<SecretRequirement>,
354}
355
356/// State surface declaration.
357#[derive(Clone, Debug, PartialEq, Eq, Default)]
358#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
359#[cfg_attr(feature = "schemars", derive(JsonSchema))]
360pub struct StateCapabilities {
361    /// Whether read access is required.
362    #[cfg_attr(feature = "serde", serde(default))]
363    pub read: bool,
364    /// Whether write access is required.
365    #[cfg_attr(feature = "serde", serde(default))]
366    pub write: bool,
367}
368
369/// Messaging capability declaration.
370#[derive(Clone, Debug, PartialEq, Eq, Default)]
371#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
372#[cfg_attr(feature = "schemars", derive(JsonSchema))]
373pub struct MessagingCapabilities {
374    /// Whether the component receives inbound messages.
375    #[cfg_attr(feature = "serde", serde(default))]
376    pub inbound: bool,
377    /// Whether the component emits outbound messages.
378    #[cfg_attr(feature = "serde", serde(default))]
379    pub outbound: bool,
380}
381
382/// Events capability declaration.
383#[derive(Clone, Debug, PartialEq, Eq, Default)]
384#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
385#[cfg_attr(feature = "schemars", derive(JsonSchema))]
386pub struct EventsCapabilities {
387    /// Whether inbound events are handled.
388    #[cfg_attr(feature = "serde", serde(default))]
389    pub inbound: bool,
390    /// Whether outbound events are emitted.
391    #[cfg_attr(feature = "serde", serde(default))]
392    pub outbound: bool,
393}
394
395/// HTTP capability declaration.
396#[derive(Clone, Debug, PartialEq, Eq, Default)]
397#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
398#[cfg_attr(feature = "schemars", derive(JsonSchema))]
399pub struct HttpCapabilities {
400    /// Outbound HTTP client usage.
401    #[cfg_attr(feature = "serde", serde(default))]
402    pub client: bool,
403    /// Inbound HTTP server usage.
404    #[cfg_attr(feature = "serde", serde(default))]
405    pub server: bool,
406}
407
408/// Telemetry scoping modes.
409#[derive(Clone, Debug, PartialEq, Eq, Hash)]
410#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
411#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
412#[cfg_attr(feature = "schemars", derive(JsonSchema))]
413pub enum TelemetryScope {
414    /// Emitted telemetry is scoped to the tenant.
415    Tenant,
416    /// Scoped to the pack.
417    Pack,
418    /// Scoped per-node invocation.
419    Node,
420}
421
422/// Telemetry capability declaration.
423#[derive(Clone, Debug, PartialEq, Eq)]
424#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
425#[cfg_attr(feature = "schemars", derive(JsonSchema))]
426pub struct TelemetryCapabilities {
427    /// Maximum telemetry scope granted to the component.
428    pub scope: TelemetryScope,
429}
430
431/// Infrastructure-as-code host permissions.
432#[derive(Clone, Debug, PartialEq, Eq)]
433#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
434#[cfg_attr(feature = "schemars", derive(JsonSchema))]
435pub struct IaCCapabilities {
436    /// Whether templates/manifests may be written to a preopened path.
437    pub write_templates: bool,
438    /// Whether the component may trigger IaC plan execution via the host.
439    #[cfg_attr(feature = "serde", serde(default))]
440    pub execute_plans: bool,
441}
442
443/// Profile resolution errors.
444#[derive(Clone, Debug, PartialEq, Eq)]
445pub enum ComponentProfileError {
446    /// Requested profile is not advertised by the component.
447    UnsupportedProfile {
448        /// Profile requested by the flow.
449        requested: String,
450        /// Known supported profiles for troubleshooting.
451        supported: Vec<String>,
452    },
453}
454
455impl core::fmt::Display for ComponentProfileError {
456    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
457        match self {
458            ComponentProfileError::UnsupportedProfile {
459                requested,
460                supported,
461            } => {
462                write!(
463                    f,
464                    "profile `{requested}` is not supported; known profiles: {supported:?}"
465                )
466            }
467        }
468    }
469}
470
471#[cfg(feature = "std")]
472impl std::error::Error for ComponentProfileError {}