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