1use std::collections::HashSet;
2use std::path::{Component, Path, PathBuf};
3
4use jsonschema::{Validator, validator_for};
5use once_cell::sync::Lazy;
6use regex::Regex;
7use semver::Version;
8use serde::Serialize;
9use serde_json::Value;
10use thiserror::Error;
11
12use crate::capabilities::{
13 Capabilities, ComponentConfigurators, ComponentProfiles, validate_capabilities,
14};
15use crate::limits::Limits;
16use crate::provenance::Provenance;
17use crate::telemetry::TelemetrySpec;
18use greentic_types::component::ComponentOperation;
19use greentic_types::flow::FlowKind;
20use greentic_types::{SecretKey, SecretRequirement};
21
22static RAW_SCHEMA: &str = include_str!("../../schemas/v1/component.manifest.schema.json");
23
24static COMPILED_SCHEMA: Lazy<Validator> = Lazy::new(|| {
25 let value: Value =
26 serde_json::from_str(RAW_SCHEMA).expect("component manifest schema must be valid JSON");
27 validator_for(&value).expect("component manifest schema must compile")
28});
29
30static OPERATION_PATTERN: Lazy<Regex> =
31 Lazy::new(|| Regex::new(r"^[a-z][a-z0-9_.:-]*$").expect("valid operation regex"));
32
33#[derive(Debug, Clone, Serialize, PartialEq)]
34pub struct ComponentManifest {
35 pub id: ManifestId,
36 pub name: String,
37 pub version: Version,
38 #[serde(default)]
39 pub supports: Vec<FlowKind>,
40 pub world: World,
41 #[serde(default)]
42 pub capabilities: Capabilities,
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub secret_requirements: Vec<SecretRequirement>,
45 pub profiles: ComponentProfiles,
46 #[serde(default)]
47 pub configurators: Option<ComponentConfigurators>,
48 #[serde(default)]
49 pub limits: Option<Limits>,
50 #[serde(default)]
51 pub telemetry: Option<TelemetrySpec>,
52 pub describe_export: DescribeExport,
53 pub operations: Vec<ComponentOperation>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub default_operation: Option<String>,
56 #[serde(default)]
57 pub provenance: Option<Provenance>,
58 pub artifacts: Artifacts,
59 pub hashes: Hashes,
60}
61
62impl ComponentManifest {
63 pub fn wasm_artifact_path(&self, root: &Path) -> PathBuf {
64 root.join(&self.artifacts.component_wasm)
65 }
66}
67
68#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
69#[serde(transparent)]
70pub struct ManifestId(String);
71
72impl ManifestId {
73 fn parse(id: String) -> Result<Self, ManifestError> {
74 if id.trim().is_empty() {
75 return Err(ManifestError::EmptyField("id"));
76 }
77 Ok(Self(id))
78 }
79
80 pub fn as_str(&self) -> &str {
81 &self.0
82 }
83}
84
85impl std::fmt::Display for ManifestId {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.write_str(&self.0)
88 }
89}
90
91#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
92#[serde(transparent)]
93pub struct World(String);
94
95impl World {
96 fn parse(world: String) -> Result<Self, ManifestError> {
97 if world.trim().is_empty() {
98 return Err(ManifestError::InvalidWorld { world });
99 }
100 Ok(Self(world))
101 }
102
103 pub fn as_str(&self) -> &str {
104 &self.0
105 }
106}
107
108impl std::fmt::Display for World {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.write_str(&self.0)
111 }
112}
113
114#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
115#[serde(transparent)]
116pub struct DescribeExport(String);
117
118impl DescribeExport {
119 fn parse(export: String) -> Result<Self, ManifestError> {
120 if export.trim().is_empty() {
121 return Err(ManifestError::InvalidDescribeExport {
122 export,
123 reason: "describe_export cannot be empty".into(),
124 });
125 }
126 Ok(Self(export))
127 }
128
129 pub fn as_str(&self) -> &str {
130 &self.0
131 }
132
133 pub fn kind(&self) -> DescribeKind {
134 if self.0.contains(':') && self.0.contains('/') {
135 DescribeKind::WitWorld
136 } else {
137 DescribeKind::Export
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum DescribeKind {
144 Export,
145 WitWorld,
146}
147
148#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
149pub struct Artifacts {
150 component_wasm: PathBuf,
151}
152
153impl Artifacts {
154 pub fn component_wasm(&self) -> &Path {
155 &self.component_wasm
156 }
157}
158
159#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
160pub struct Hashes {
161 pub component_wasm: WasmHash,
162}
163
164#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
165#[serde(transparent)]
166pub struct WasmHash(String);
167
168impl WasmHash {
169 fn parse(hash: String) -> Result<Self, ManifestError> {
170 let Some(rest) = hash.strip_prefix("blake3:") else {
171 return Err(ManifestError::InvalidHashFormat { hash });
172 };
173 if rest.len() != 64 || !rest.chars().all(|c| c.is_ascii_hexdigit()) {
174 return Err(ManifestError::InvalidHashFormat {
175 hash: format!("blake3:{rest}"),
176 });
177 }
178 Ok(Self(format!("blake3:{rest}")))
179 }
180
181 pub fn algorithm(&self) -> &str {
182 "blake3"
183 }
184
185 pub fn digest(&self) -> &str {
186 &self.0[7..]
187 }
188
189 pub fn as_str(&self) -> &str {
190 &self.0
191 }
192}
193
194pub fn schema() -> &'static str {
195 RAW_SCHEMA
196}
197
198pub fn parse_manifest(raw: &str) -> Result<ComponentManifest, ManifestError> {
199 let mut value: Value = serde_json::from_str(raw)?;
200 normalize_state_delete(&mut value);
201 validate_value(&value)?;
202 let raw_manifest: RawManifest = serde_json::from_value(value)?;
203 raw_manifest.try_into()
204}
205
206pub fn validate_manifest(raw: &str) -> Result<(), ManifestError> {
207 let value: Value = serde_json::from_str(raw)?;
208 validate_value(&value)
209}
210
211fn validate_value(value: &Value) -> Result<(), ManifestError> {
212 let errors: Vec<String> = COMPILED_SCHEMA
213 .iter_errors(value)
214 .map(|err| err.to_string())
215 .collect();
216 if errors.is_empty() {
217 Ok(())
218 } else {
219 Err(ManifestError::Schema(errors.join(", ")))
220 }
221}
222
223fn normalize_state_delete(value: &mut Value) {
224 let state = value
225 .get_mut("capabilities")
226 .and_then(|caps| caps.get_mut("host"))
227 .and_then(|host| host.get_mut("state"));
228 if let Some(state) = state {
229 let delete_enabled = state
230 .get("delete")
231 .and_then(|value| value.as_bool())
232 .unwrap_or(false);
233 if delete_enabled {
234 state
235 .as_object_mut()
236 .map(|obj| obj.insert("write".to_string(), Value::Bool(true)));
237 }
238 }
239}
240
241#[derive(Debug, Error)]
242pub enum ManifestError {
243 #[error("manifest json parse failed: {0}")]
244 Json(#[from] serde_json::Error),
245 #[error("manifest schema validation failed: {0}")]
246 Schema(String),
247 #[error("world identifier is invalid: `{world}`")]
248 InvalidWorld { world: String },
249 #[error("manifest field `{0}` cannot be empty")]
250 EmptyField(&'static str),
251 #[error("component must expose at least one operation")]
252 MissingOperations,
253 #[error("operation `{operation}` is invalid")]
254 InvalidOperation { operation: String },
255 #[error("duplicate operation `{0}` detected")]
256 DuplicateOperation(String),
257 #[error("default_operation `{operation}` must match one of the declared operations")]
258 InvalidDefaultOperation { operation: String },
259 #[error("component must support at least one flow kind")]
260 MissingSupports,
261 #[error("profiles.supported must include at least one profile identifier")]
262 MissingProfiles,
263 #[error("profiles.default `{default}` must be one of the supported profiles")]
264 InvalidProfileDefault { default: String },
265 #[error("invalid semantic version `{version}`: {source}")]
266 InvalidVersion {
267 version: String,
268 #[source]
269 source: semver::Error,
270 },
271 #[error("invalid describe export `{export}`: {reason}")]
272 InvalidDescribeExport { export: String, reason: String },
273 #[error("component wasm path must be relative (got `{path}`)")]
274 InvalidArtifactPath { path: String },
275 #[error("component wasm hash must be blake3:<hex> (got `{hash}`)")]
276 InvalidHashFormat { hash: String },
277 #[error("capability validation failed: {0}")]
278 Capability(String),
279 #[error("duplicate secret requirement `{0}` detected")]
280 DuplicateSecretRequirement(String),
281 #[error("secret requirement `{key}` is invalid: {reason}")]
282 InvalidSecretRequirement { key: String, reason: String },
283 #[error("limits invalid: {0}")]
284 Limits(String),
285 #[error("provenance invalid: {0}")]
286 Provenance(String),
287}
288
289#[derive(Debug, serde::Deserialize)]
290struct RawManifest {
291 id: String,
292 name: String,
293 version: String,
294 world: String,
295 #[serde(default)]
296 supports: Vec<FlowKind>,
297 #[serde(default)]
298 capabilities: Capabilities,
299 #[serde(default)]
300 secret_requirements: Vec<SecretRequirement>,
301 #[serde(default)]
302 profiles: ComponentProfiles,
303 #[serde(default)]
304 configurators: Option<ComponentConfigurators>,
305 #[serde(default)]
306 limits: Option<Limits>,
307 #[serde(default)]
308 telemetry: Option<TelemetrySpec>,
309 describe_export: String,
310 operations: Vec<ComponentOperation>,
311 #[serde(default)]
312 default_operation: Option<String>,
313 #[serde(default)]
314 provenance: Option<Provenance>,
315 artifacts: RawArtifacts,
316 hashes: RawHashes,
317}
318
319impl TryFrom<RawManifest> for ComponentManifest {
320 type Error = ManifestError;
321
322 fn try_from(raw: RawManifest) -> Result<Self, Self::Error> {
323 if raw.name.trim().is_empty() {
324 return Err(ManifestError::EmptyField("name"));
325 }
326
327 let id = ManifestId::parse(raw.id)?;
328 let world = World::parse(raw.world)?;
329 let version =
330 Version::parse(&raw.version).map_err(|source| ManifestError::InvalidVersion {
331 version: raw.version,
332 source,
333 })?;
334 let describe_export = DescribeExport::parse(raw.describe_export)?;
335 let artifacts = Artifacts::try_from(raw.artifacts)?;
336 let hashes = Hashes::try_from(raw.hashes)?;
337
338 if raw.supports.is_empty() {
339 return Err(ManifestError::MissingSupports);
340 }
341
342 validate_profiles(&raw.profiles)?;
343
344 if let Some(configurators) = &raw.configurators {
345 validate_configurators(configurators)?;
346 }
347
348 validate_capabilities(&raw.capabilities)
349 .map_err(|err| ManifestError::Capability(err.to_string()))?;
350
351 validate_secret_requirements(&raw.secret_requirements)?;
352
353 if let Some(limits) = &raw.limits {
354 limits
355 .validate()
356 .map_err(|err| ManifestError::Limits(err.to_string()))?;
357 }
358
359 if let Some(provenance) = &raw.provenance {
360 provenance
361 .validate()
362 .map_err(|err| ManifestError::Provenance(err.to_string()))?;
363 }
364
365 if raw.operations.is_empty() {
366 return Err(ManifestError::MissingOperations);
367 }
368 let mut seen_operations = HashSet::new();
369 for operation in &raw.operations {
370 if !seen_operations.insert(&operation.name) {
371 return Err(ManifestError::DuplicateOperation(operation.name.clone()));
372 }
373 if !OPERATION_PATTERN.is_match(&operation.name) {
374 return Err(ManifestError::InvalidOperation {
375 operation: operation.name.clone(),
376 });
377 }
378 }
379 if let Some(default_operation) = &raw.default_operation
380 && !raw
381 .operations
382 .iter()
383 .any(|op| op.name == *default_operation)
384 {
385 return Err(ManifestError::InvalidDefaultOperation {
386 operation: default_operation.clone(),
387 });
388 }
389
390 Ok(Self {
391 id,
392 name: raw.name,
393 version,
394 world,
395 supports: raw.supports,
396 capabilities: raw.capabilities,
397 secret_requirements: raw.secret_requirements,
398 profiles: raw.profiles,
399 configurators: raw.configurators,
400 limits: raw.limits,
401 telemetry: raw.telemetry,
402 describe_export,
403 operations: raw.operations,
404 default_operation: raw.default_operation,
405 provenance: raw.provenance,
406 artifacts,
407 hashes,
408 })
409 }
410}
411
412#[derive(Debug, serde::Deserialize)]
413struct RawArtifacts {
414 component_wasm: String,
415}
416
417impl TryFrom<RawArtifacts> for Artifacts {
418 type Error = ManifestError;
419
420 fn try_from(value: RawArtifacts) -> Result<Self, Self::Error> {
421 ensure_relative(&value.component_wasm)?;
422 Ok(Artifacts {
423 component_wasm: PathBuf::from(value.component_wasm),
424 })
425 }
426}
427
428#[derive(Debug, serde::Deserialize)]
429struct RawHashes {
430 component_wasm: String,
431}
432
433impl TryFrom<RawHashes> for Hashes {
434 type Error = ManifestError;
435
436 fn try_from(value: RawHashes) -> Result<Self, Self::Error> {
437 Ok(Hashes {
438 component_wasm: WasmHash::parse(value.component_wasm)?,
439 })
440 }
441}
442
443fn ensure_relative(path: &str) -> Result<(), ManifestError> {
444 let path_buf = PathBuf::from(path);
445 if path_buf.is_absolute() {
446 return Err(ManifestError::InvalidArtifactPath {
447 path: path.to_string(),
448 });
449 }
450 if matches!(path_buf.components().next(), Some(Component::Prefix(_))) {
451 return Err(ManifestError::InvalidArtifactPath {
452 path: path.to_string(),
453 });
454 }
455 Ok(())
456}
457
458fn validate_secret_requirements(requirements: &[SecretRequirement]) -> Result<(), ManifestError> {
459 let mut seen = std::collections::HashSet::new();
460 for req in requirements {
461 if !seen.insert(req.key.as_str().to_string()) {
462 return Err(ManifestError::DuplicateSecretRequirement(
463 req.key.as_str().to_string(),
464 ));
465 }
466
467 SecretKey::new(req.key.as_str()).map_err(|err| {
468 ManifestError::InvalidSecretRequirement {
469 key: req.key.as_str().to_string(),
470 reason: err.to_string(),
471 }
472 })?;
473
474 let scope = req
475 .scope
476 .as_ref()
477 .ok_or_else(|| ManifestError::InvalidSecretRequirement {
478 key: req.key.as_str().to_string(),
479 reason: "scope must include env and tenant".into(),
480 })?;
481
482 if scope.env.trim().is_empty() {
483 return Err(ManifestError::InvalidSecretRequirement {
484 key: req.key.as_str().to_string(),
485 reason: "scope.env must not be empty".into(),
486 });
487 }
488 if scope.tenant.trim().is_empty() {
489 return Err(ManifestError::InvalidSecretRequirement {
490 key: req.key.as_str().to_string(),
491 reason: "scope.tenant must not be empty".into(),
492 });
493 }
494 if let Some(team) = &scope.team
495 && team.trim().is_empty()
496 {
497 return Err(ManifestError::InvalidSecretRequirement {
498 key: req.key.as_str().to_string(),
499 reason: "scope.team must not be empty when provided".into(),
500 });
501 }
502
503 if req.format.is_none() {
504 return Err(ManifestError::InvalidSecretRequirement {
505 key: req.key.as_str().to_string(),
506 reason: "format must be specified".into(),
507 });
508 }
509
510 if let Some(schema) = &req.schema
511 && !schema.is_object()
512 {
513 return Err(ManifestError::InvalidSecretRequirement {
514 key: req.key.as_str().to_string(),
515 reason: "schema must be an object when provided".into(),
516 });
517 }
518 }
519 Ok(())
520}
521
522fn validate_profiles(profiles: &ComponentProfiles) -> Result<(), ManifestError> {
523 if profiles.supported.is_empty() {
524 return Err(ManifestError::MissingProfiles);
525 }
526 if let Some(default) = &profiles.default
527 && !profiles.supported.iter().any(|entry| entry == default)
528 {
529 return Err(ManifestError::InvalidProfileDefault {
530 default: default.clone(),
531 });
532 }
533 Ok(())
534}
535
536fn validate_configurators(_configurators: &ComponentConfigurators) -> Result<(), ManifestError> {
537 Ok(())
539}