greentic_types/
component.rs

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