greentic_component/
capabilities.rs1pub 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
9pub 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 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#[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 {}