greentic_component/
capabilities.rs

1pub use greentic_types::component::{
2    ComponentCapabilities as Capabilities, ComponentConfigurators, ComponentProfiles,
3    EnvCapabilities, EventsCapabilities, FilesystemCapabilities, FilesystemMode, FilesystemMount,
4    HostCapabilities, HttpCapabilities, IaCCapabilities, MessagingCapabilities,
5    SecretsCapabilities, StateCapabilities, TelemetryCapabilities, TelemetryScope,
6    WasiCapabilities,
7};
8
9/// Validates a capability declaration, ensuring basic structural correctness.
10pub fn validate_capabilities(caps: &Capabilities) -> Result<(), CapabilityError> {
11    validate_wasi(&caps.wasi)?;
12    validate_host(&caps.host)?;
13    Ok(())
14}
15
16fn validate_wasi(wasi: &WasiCapabilities) -> Result<(), CapabilityError> {
17    if let Some(fs) = &wasi.filesystem {
18        validate_filesystem(fs)?;
19    }
20    if let Some(env) = &wasi.env {
21        validate_env(env)?;
22    }
23    Ok(())
24}
25
26fn validate_filesystem(fs: &FilesystemCapabilities) -> Result<(), CapabilityError> {
27    if fs.mode != FilesystemMode::None && fs.mounts.is_empty() {
28        return Err(CapabilityError::invalid(
29            "wasi.filesystem.mounts",
30            "filesystem mounts must be declared when exposing the filesystem",
31        ));
32    }
33    for mount in &fs.mounts {
34        validate_mount(mount)?;
35    }
36    Ok(())
37}
38
39fn validate_mount(mount: &FilesystemMount) -> Result<(), CapabilityError> {
40    if mount.name.trim().is_empty() {
41        return Err(CapabilityError::invalid(
42            "wasi.filesystem.mounts[].name",
43            "mount name cannot be empty",
44        ));
45    }
46    if mount.host_class.trim().is_empty() {
47        return Err(CapabilityError::invalid(
48            "wasi.filesystem.mounts[].host_class",
49            "host_class must describe a storage class",
50        ));
51    }
52    if mount.guest_path.trim().is_empty() {
53        return Err(CapabilityError::invalid(
54            "wasi.filesystem.mounts[].guest_path",
55            "guest_path cannot be empty",
56        ));
57    }
58    Ok(())
59}
60
61fn validate_env(env: &EnvCapabilities) -> Result<(), CapabilityError> {
62    for var in &env.allow {
63        if var.trim().is_empty() {
64            return Err(CapabilityError::invalid(
65                "wasi.env.allow[]",
66                "environment variable names cannot be empty",
67            ));
68        }
69    }
70    Ok(())
71}
72
73fn validate_host(host: &HostCapabilities) -> Result<(), CapabilityError> {
74    if let Some(secrets) = &host.secrets {
75        validate_secrets(secrets)?;
76    }
77    if let Some(state) = &host.state
78        && !state.read
79        && !state.write
80    {
81        return Err(CapabilityError::invalid(
82            "host.state",
83            "state capability must enable read and/or write",
84        ));
85    }
86    if let Some(telemetry) = &host.telemetry {
87        validate_telemetry(telemetry)?;
88    }
89    if let Some(iac) = &host.iac {
90        validate_iac(iac)?;
91    }
92    Ok(())
93}
94
95fn validate_secrets(secrets: &SecretsCapabilities) -> Result<(), CapabilityError> {
96    for key in &secrets.required {
97        if key.trim().is_empty() {
98            return Err(CapabilityError::invalid(
99                "host.secrets.required[]",
100                "secret identifiers cannot be empty",
101            ));
102        }
103    }
104    Ok(())
105}
106
107fn validate_telemetry(telemetry: &TelemetryCapabilities) -> Result<(), CapabilityError> {
108    // No structural validation beyond ensuring the enum is populated.
109    let _ = telemetry.scope;
110    Ok(())
111}
112
113fn validate_iac(iac: &IaCCapabilities) -> Result<(), CapabilityError> {
114    if !iac.write_templates && !iac.execute_plans {
115        return Err(CapabilityError::invalid(
116            "host.iac",
117            "iac capability must enable template writes and/or plan execution",
118        ));
119    }
120    Ok(())
121}
122
123/// Error produced when capability declarations are malformed.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct CapabilityError {
126    pub path: &'static str,
127    pub message: String,
128}
129
130impl CapabilityError {
131    pub fn invalid(path: &'static str, message: impl Into<String>) -> Self {
132        Self {
133            path,
134            message: message.into(),
135        }
136    }
137}
138
139impl core::fmt::Display for CapabilityError {
140    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
141        write!(f, "invalid capability `{}`: {}", self.path, self.message)
142    }
143}
144
145impl std::error::Error for CapabilityError {}