greentic_component/scaffold/
runtime_capabilities.rs1#![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}