Skip to main content

greentic_component/scaffold/
runtime_capabilities.rs

1#![cfg(feature = "cli")]
2
3use std::collections::BTreeMap;
4
5use serde_json::{Value as JsonValue, json};
6
7use super::validate::ValidationError;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct RuntimeCapabilitiesInput {
11    pub filesystem_mode: String,
12    pub filesystem_mounts: Vec<RuntimeFilesystemMount>,
13    pub messaging_inbound: bool,
14    pub messaging_outbound: bool,
15    pub events_inbound: bool,
16    pub events_outbound: bool,
17    pub http_client: bool,
18    pub http_server: bool,
19    pub state_read: bool,
20    pub state_write: bool,
21    pub state_delete: bool,
22    pub telemetry_scope: String,
23    pub telemetry_span_prefix: Option<String>,
24    pub telemetry_attributes: BTreeMap<String, String>,
25    pub secret_keys: Vec<String>,
26    pub secret_env: String,
27    pub secret_tenant: String,
28    pub secret_format: String,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct RuntimeFilesystemMount {
33    pub name: String,
34    pub host_class: String,
35    pub guest_path: String,
36}
37
38impl Default for RuntimeCapabilitiesInput {
39    fn default() -> Self {
40        Self {
41            filesystem_mode: "none".to_string(),
42            filesystem_mounts: Vec::new(),
43            messaging_inbound: false,
44            messaging_outbound: false,
45            events_inbound: false,
46            events_outbound: false,
47            http_client: false,
48            http_server: false,
49            state_read: false,
50            state_write: false,
51            state_delete: false,
52            telemetry_scope: "node".to_string(),
53            telemetry_span_prefix: None,
54            telemetry_attributes: BTreeMap::new(),
55            secret_keys: Vec::new(),
56            secret_env: "dev".to_string(),
57            secret_tenant: "default".to_string(),
58            secret_format: "text".to_string(),
59        }
60    }
61}
62
63impl RuntimeCapabilitiesInput {
64    fn effective_filesystem_mounts(&self) -> &[RuntimeFilesystemMount] {
65        if self.filesystem_mode == "none" {
66            &[]
67        } else {
68            &self.filesystem_mounts
69        }
70    }
71
72    pub fn manifest_secret_requirements(&self) -> JsonValue {
73        JsonValue::Array(
74            self.secret_keys
75                .iter()
76                .map(|key| {
77                    json!({
78                        "key": key,
79                        "required": true,
80                        "scope": {
81                            "env": self.secret_env,
82                            "tenant": self.secret_tenant
83                        },
84                        "format": self.secret_format
85                    })
86                })
87                .collect(),
88        )
89    }
90
91    pub fn manifest_capabilities(&self) -> JsonValue {
92        let mut wasi = serde_json::Map::new();
93        wasi.insert(
94            "filesystem".to_string(),
95            json!({
96                "mode": self.filesystem_mode,
97                "mounts": self.effective_filesystem_mounts().iter().map(|mount| {
98                    json!({
99                        "name": mount.name,
100                        "host_class": mount.host_class,
101                        "guest_path": mount.guest_path
102                    })
103                }).collect::<Vec<_>>()
104            }),
105        );
106        wasi.insert("random".to_string(), JsonValue::Bool(true));
107        wasi.insert("clocks".to_string(), JsonValue::Bool(true));
108
109        let mut host = serde_json::Map::new();
110        if self.messaging_inbound || self.messaging_outbound {
111            host.insert(
112                "messaging".to_string(),
113                json!({
114                    "inbound": self.messaging_inbound,
115                    "outbound": self.messaging_outbound
116                }),
117            );
118        }
119        if self.events_inbound || self.events_outbound {
120            host.insert(
121                "events".to_string(),
122                json!({
123                    "inbound": self.events_inbound,
124                    "outbound": self.events_outbound
125                }),
126            );
127        }
128        host.insert(
129            "telemetry".to_string(),
130            json!({
131                "scope": self.telemetry_scope
132            }),
133        );
134        host.insert(
135            "secrets".to_string(),
136            json!({
137                "required": self.manifest_secret_requirements()
138            }),
139        );
140        if self.http_client || self.http_server {
141            host.insert(
142                "http".to_string(),
143                json!({
144                    "client": self.http_client,
145                    "server": self.http_server
146                }),
147            );
148        }
149        let state_write = self.state_write || self.state_delete;
150        if self.state_read || state_write || self.state_delete {
151            host.insert(
152                "state".to_string(),
153                json!({
154                    "read": self.state_read,
155                    "write": state_write,
156                    "delete": self.state_delete
157                }),
158            );
159        }
160
161        let mut capabilities = serde_json::Map::new();
162        capabilities.insert("wasi".to_string(), JsonValue::Object(wasi));
163        capabilities.insert("host".to_string(), JsonValue::Object(host));
164        JsonValue::Object(capabilities)
165    }
166
167    pub fn manifest_telemetry(&self) -> Option<JsonValue> {
168        self.telemetry_span_prefix.as_ref().map(|prefix| {
169            json!({
170                "span_prefix": prefix,
171                "attributes": self.telemetry_attributes,
172                "emit_node_spans": true
173            })
174        })
175    }
176}
177
178pub fn parse_filesystem_mode(value: &str) -> Result<String, ValidationError> {
179    match value.trim() {
180        "none" | "read_only" | "sandbox" => Ok(value.trim().to_string()),
181        other => Err(ValidationError::InvalidFilesystemMode(other.to_string())),
182    }
183}
184
185pub fn parse_telemetry_scope(value: &str) -> Result<String, ValidationError> {
186    match value.trim() {
187        "tenant" | "pack" | "node" => Ok(value.trim().to_string()),
188        other => Err(ValidationError::InvalidTelemetryScope(other.to_string())),
189    }
190}
191
192pub fn parse_secret_format(value: &str) -> Result<String, ValidationError> {
193    match value.trim() {
194        "bytes" | "text" | "json" => Ok(value.trim().to_string()),
195        other => Err(ValidationError::InvalidSecretFormat(other.to_string())),
196    }
197}
198
199pub fn parse_filesystem_mount(value: &str) -> Result<RuntimeFilesystemMount, ValidationError> {
200    let mut parts = value.splitn(3, ':').map(str::trim);
201    let name = parts.next().unwrap_or_default();
202    let host_class = parts.next().unwrap_or_default();
203    let guest_path = parts.next().unwrap_or_default();
204    if name.is_empty() || host_class.is_empty() || guest_path.is_empty() {
205        return Err(ValidationError::InvalidFilesystemMount(value.to_string()));
206    }
207    Ok(RuntimeFilesystemMount {
208        name: name.to_string(),
209        host_class: host_class.to_string(),
210        guest_path: guest_path.to_string(),
211    })
212}
213
214pub fn parse_telemetry_attributes(
215    values: &[String],
216) -> Result<BTreeMap<String, String>, ValidationError> {
217    let mut attributes = BTreeMap::new();
218    for value in values {
219        let Some((key, attr_value)) = value.split_once('=') else {
220            return Err(ValidationError::InvalidTelemetryAttribute(value.clone()));
221        };
222        let key = key.trim();
223        let attr_value = attr_value.trim();
224        if key.is_empty() || attr_value.is_empty() {
225            return Err(ValidationError::InvalidTelemetryAttribute(value.clone()));
226        }
227        attributes.insert(key.to_string(), attr_value.to_string());
228    }
229    Ok(attributes)
230}