Skip to main content

rill_runtime_protocol/
lib.rs

1//! Stable, versioned contracts shared by Rill Runtime and its hosts.
2//!
3//! ## IPC API versions
4//!
5//! | Version | Introduced in | Changes |
6//! |---|---|---|
7//! | 1 | 0.5.0 | Original handshake, health, invoke |
8//! | 2 | 0.7.0 | Handshake response gains handler identity and effective capabilities |
9//!
10//! The runtime accepts both v1 and v2 requests. v1 clients receive
11//! [`RuntimeResponse`] (no handler fields). v2 clients receive
12//! [`RuntimeResponseV2`] (with handler identity). The two wire schemas are
13//! independently frozen with fixture tests.
14
15use serde::{Deserialize, Serialize};
16
17/// Minimum IPC API version the runtime still accepts.
18pub const MIN_RUNTIME_API_VERSION: u32 = 1;
19/// Latest IPC API version supported by this crate.
20pub const RUNTIME_API_VERSION: u32 = 2;
21/// Signed model-pack container version.
22pub const MODEL_PACK_FORMAT_VERSION: u32 = 1;
23/// Signed handler-pack container version.
24pub const HANDLER_PACKAGE_FORMAT_VERSION: u32 = 1;
25/// Handler ABI version (independent of IPC API version).
26pub const HANDLER_API_VERSION: u32 = 1;
27/// Persisted host/runtime state envelope version.
28pub const RUNTIME_STATE_FORMAT_VERSION: u32 = 1;
29/// Signed release-index schema understood by independent updaters.
30pub const RELEASE_INDEX_SCHEMA_VERSION: u32 = 2;
31/// Hard upper bound for one newline-delimited IPC message.
32pub const MAX_MESSAGE_BYTES: usize = 1024 * 1024;
33
34pub const RUNTIME_ARTIFACT_ID: &str = "rill-runtime";
35
36// ---------------------------------------------------------------------------
37// Stable IPC error codes
38// ---------------------------------------------------------------------------
39
40/// Stable IPC error code constants.
41///
42/// Every `RuntimeResponse::Error` / `RuntimeResponseV2::Error` `code` field
43/// produced by the runtime is one of the constants in this module. The codes
44/// are frozen for the entire 1.x cycle: existing codes are never renamed, and
45/// new codes may only be added (additive).
46///
47/// The runtime constructs error responses exclusively from these constants.
48/// Hosts and clients may switch on the string values; the constants are
49/// exported so that downstream Rust code does not have to inline string
50/// literals.
51pub mod error_code {
52    /// Request body was not valid protocol JSON.
53    pub const INVALID_JSON: &str = "invalidJson";
54    /// `requestId` was missing, empty, or longer than 128 characters.
55    pub const INVALID_REQUEST_ID: &str = "invalidRequestId";
56    /// `apiVersion` was outside `[MIN_RUNTIME_API_VERSION, RUNTIME_API_VERSION]`.
57    pub const INCOMPATIBLE_API_VERSION: &str = "incompatibleApiVersion";
58    /// `clientName` / `clientVersion` failed length or emptiness checks.
59    pub const INVALID_CLIENT_IDENTITY: &str = "invalidClientIdentity";
60    /// `Invoke` capability is not in the effective capability set.
61    pub const UNSUPPORTED_CAPABILITY: &str = "unsupportedCapability";
62    /// `Invoke` was issued but no handler is registered.
63    pub const NO_INVOKE_HANDLER: &str = "noInvokeHandler";
64    /// Handler exceeded the wall-clock deadline. Retryable.
65    pub const HANDLER_TIMEOUT: &str = "handlerTimeout";
66    /// Handler trapped (unreachable, out-of-bounds, stack overflow, …).
67    pub const HANDLER_TRAP: &str = "handlerTrap";
68    /// Handler output exceeded the host-side size limit.
69    pub const HANDLER_OUTPUT_TOO_LARGE: &str = "handlerOutputTooLarge";
70    /// Handler output was not valid JSON.
71    pub const HANDLER_INVALID_OUTPUT: &str = "handlerInvalidOutput";
72    /// Handler reported an internal error (covers all four WIT
73    /// `handler-error` variants on the wire for backwards compatibility).
74    pub const HANDLER_INTERNAL_ERROR: &str = "handlerInternalError";
75
76    /// All frozen error codes in alphabetical order.
77    ///
78    /// This slice is used by tests and by the runtime's error-code allowlist
79    /// check. Adding a new code requires appending to this slice; the order
80    /// is part of the frozen surface so test fixtures remain stable.
81    pub const FROZEN_CODES: &[&str] = &[
82        HANDLER_INTERNAL_ERROR,
83        HANDLER_INVALID_OUTPUT,
84        HANDLER_OUTPUT_TOO_LARGE,
85        HANDLER_TIMEOUT,
86        HANDLER_TRAP,
87        INCOMPATIBLE_API_VERSION,
88        INVALID_CLIENT_IDENTITY,
89        INVALID_JSON,
90        INVALID_REQUEST_ID,
91        NO_INVOKE_HANDLER,
92        UNSUPPORTED_CAPABILITY,
93    ];
94
95    /// Returns `true` if `code` is one of the frozen 1.x error codes.
96    pub fn is_frozen(code: &str) -> bool {
97        FROZEN_CODES.contains(&code)
98    }
99}
100
101// ---------------------------------------------------------------------------
102// Model pack manifest
103// ---------------------------------------------------------------------------
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
106#[serde(rename_all = "camelCase", deny_unknown_fields)]
107pub struct ModelPackManifest {
108    pub format_version: u32,
109    pub id: String,
110    pub version: String,
111    pub runtime_api_version: u32,
112    pub min_runtime_version: String,
113    pub publisher_key_id: String,
114    pub capabilities: Vec<String>,
115}
116
117impl ModelPackManifest {
118    pub fn validate_shape(&self) -> Result<(), &'static str> {
119        if self.format_version != MODEL_PACK_FORMAT_VERSION {
120            return Err("unsupported model-pack format version");
121        }
122        if self.runtime_api_version != RUNTIME_API_VERSION {
123            return Err("unsupported runtime API version");
124        }
125        if self.id.is_empty() || self.id.len() > 96 {
126            return Err("invalid model-pack id");
127        }
128        if self.version.is_empty() || self.version.len() > 48 {
129            return Err("invalid model-pack version");
130        }
131        if self.publisher_key_id.is_empty() || self.publisher_key_id.len() > 96 {
132            return Err("invalid publisher key id");
133        }
134        Self::validate_capabilities(&self.capabilities)?;
135        Ok(())
136    }
137
138    pub fn validate_capabilities(capabilities: &[String]) -> Result<(), &'static str> {
139        if capabilities.is_empty() || capabilities.len() > 32 {
140            return Err("invalid capabilities list");
141        }
142        if capabilities
143            .iter()
144            .any(|capability| capability.is_empty() || capability.len() > 96)
145        {
146            return Err("invalid capability string");
147        }
148        let mut seen = std::collections::HashSet::new();
149        if !capabilities
150            .iter()
151            .all(|capability| seen.insert(capability.clone()))
152        {
153            return Err("duplicate capability");
154        }
155        Ok(())
156    }
157}
158
159// ---------------------------------------------------------------------------
160// Handler pack manifest
161// ---------------------------------------------------------------------------
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164#[serde(rename_all = "camelCase", deny_unknown_fields)]
165pub struct HandlerPackManifest {
166    pub format_version: u32,
167    pub id: String,
168    pub version: String,
169    pub handler_api_version: u32,
170    pub min_runtime_version: String,
171    pub publisher_key_id: String,
172    pub capabilities: Vec<String>,
173    pub module_sha256: String,
174    pub module_size: u64,
175}
176
177impl HandlerPackManifest {
178    pub fn validate_shape(&self) -> Result<(), &'static str> {
179        if self.format_version != HANDLER_PACKAGE_FORMAT_VERSION {
180            return Err("unsupported handler-pack format version");
181        }
182        if self.handler_api_version != HANDLER_API_VERSION {
183            return Err("unsupported handler API version");
184        }
185        if self.id.is_empty() || self.id.len() > 96 {
186            return Err("invalid handler id");
187        }
188        if self.version.is_empty() || self.version.len() > 48 {
189            return Err("invalid handler version");
190        }
191        if self.publisher_key_id.is_empty() || self.publisher_key_id.len() > 96 {
192            return Err("invalid handler publisher key id");
193        }
194        if self.min_runtime_version.is_empty() || self.min_runtime_version.len() > 48 {
195            return Err("invalid minimum runtime version");
196        }
197        ModelPackManifest::validate_capabilities(&self.capabilities)?;
198        if self.module_sha256.len() != 64
199            || !self
200                .module_sha256
201                .bytes()
202                .all(|byte| byte.is_ascii_hexdigit())
203        {
204            return Err("invalid module SHA-256");
205        }
206        if self.module_size == 0 || self.module_size > 4 * 1024 * 1024 {
207            return Err("invalid module size");
208        }
209        Ok(())
210    }
211}
212
213// ---------------------------------------------------------------------------
214// Release index
215// ---------------------------------------------------------------------------
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
218#[serde(rename_all = "camelCase")]
219pub enum ReleaseArtifactKind {
220    Runtime,
221    Model,
222    Handler,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
226#[serde(rename_all = "camelCase", deny_unknown_fields)]
227pub struct ReleaseArtifact {
228    pub kind: ReleaseArtifactKind,
229    pub id: String,
230    pub version: String,
231    pub runtime_api_version: u32,
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub target_os: Option<String>,
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub target_arch: Option<String>,
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub handler_api_version: Option<u32>,
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub min_runtime_version: Option<String>,
240    pub url: String,
241    pub sha256: String,
242    pub size: u64,
243}
244
245impl ReleaseArtifact {
246    pub fn validate_shape(&self) -> Result<(), &'static str> {
247        if self.id.is_empty() || self.id.len() > 96 {
248            return Err("invalid artifact id");
249        }
250        if self.version.is_empty() || self.version.len() > 48 {
251            return Err("invalid artifact version");
252        }
253        if self.url.is_empty() || self.url.len() > 2048 {
254            return Err("invalid artifact URL");
255        }
256        if self.sha256.len() != 64 || !self.sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
257            return Err("invalid artifact SHA-256");
258        }
259        if self.size == 0 || self.size > 128 * 1024 * 1024 {
260            return Err("invalid artifact size");
261        }
262        match self.kind {
263            ReleaseArtifactKind::Runtime => {
264                if self.runtime_api_version != RUNTIME_API_VERSION {
265                    return Err("unsupported artifact runtime API version");
266                }
267                if self.id != RUNTIME_ARTIFACT_ID
268                    || self.target_os.as_deref().is_none_or(str::is_empty)
269                    || self.target_arch.as_deref().is_none_or(str::is_empty)
270                {
271                    return Err("runtime artifact requires a target OS and architecture");
272                }
273                if self.handler_api_version.is_some() || self.min_runtime_version.is_some() {
274                    return Err("runtime artifact must not carry handler fields");
275                }
276            }
277            ReleaseArtifactKind::Model => {
278                if self.runtime_api_version != RUNTIME_API_VERSION {
279                    return Err("unsupported artifact runtime API version");
280                }
281                if self.target_os.is_some()
282                    || self.target_arch.is_some()
283                    || self.handler_api_version.is_some()
284                    || self.min_runtime_version.is_some()
285                {
286                    return Err("model artifact must be platform independent");
287                }
288            }
289            ReleaseArtifactKind::Handler => {
290                if self.runtime_api_version != RUNTIME_API_VERSION {
291                    return Err("unsupported artifact runtime API version");
292                }
293                if self.target_os.is_some() || self.target_arch.is_some() {
294                    return Err("handler artifact must be platform independent");
295                }
296                let handler_api = self
297                    .handler_api_version
298                    .ok_or("handler artifact requires handler API version")?;
299                if handler_api != HANDLER_API_VERSION {
300                    return Err("unsupported handler API version");
301                }
302                let min_runtime = self
303                    .min_runtime_version
304                    .as_deref()
305                    .ok_or("handler artifact requires minimum runtime version")?;
306                if min_runtime.is_empty() || min_runtime.len() > 48 {
307                    return Err("invalid minimum runtime version");
308                }
309            }
310        }
311        Ok(())
312    }
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
316#[serde(rename_all = "camelCase", deny_unknown_fields)]
317pub struct ReleaseIndexPayload {
318    pub schema_version: u32,
319    pub channel: String,
320    pub generated_at: String,
321    pub publisher_key_id: String,
322    pub artifacts: Vec<ReleaseArtifact>,
323}
324
325impl ReleaseIndexPayload {
326    pub fn validate_shape(&self) -> Result<(), &'static str> {
327        if self.schema_version != RELEASE_INDEX_SCHEMA_VERSION {
328            return Err("unsupported release-index schema");
329        }
330        if !matches!(self.channel.as_str(), "stable" | "candidate") {
331            return Err("unsupported release channel");
332        }
333        if self.generated_at.is_empty() || self.generated_at.len() > 64 {
334            return Err("invalid release-index timestamp");
335        }
336        if self.publisher_key_id.is_empty() || self.publisher_key_id.len() > 96 {
337            return Err("invalid release-index publisher");
338        }
339        if self.artifacts.is_empty() || self.artifacts.len() > 64 {
340            return Err("invalid release-index artifact count");
341        }
342        for artifact in &self.artifacts {
343            artifact.validate_shape()?;
344        }
345        Ok(())
346    }
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
350#[serde(rename_all = "camelCase", deny_unknown_fields)]
351pub struct SignedReleaseIndex {
352    pub payload: ReleaseIndexPayload,
353    /// Lowercase hexadecimal Ed25519 signature over canonical payload JSON.
354    pub signature: String,
355}
356
357// ---------------------------------------------------------------------------
358// IPC requests (shared by v1 and v2)
359// ---------------------------------------------------------------------------
360
361#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
362#[serde(
363    tag = "method",
364    rename_all = "camelCase",
365    rename_all_fields = "camelCase",
366    deny_unknown_fields
367)]
368pub enum RuntimeRequest {
369    Handshake {
370        request_id: String,
371        api_version: u32,
372        client_name: String,
373        client_version: String,
374    },
375    Health {
376        request_id: String,
377        api_version: u32,
378    },
379    Invoke {
380        request_id: String,
381        api_version: u32,
382        capability: String,
383        input: serde_json::Value,
384    },
385}
386
387impl RuntimeRequest {
388    pub fn request_id(&self) -> &str {
389        match self {
390            Self::Handshake { request_id, .. }
391            | Self::Health { request_id, .. }
392            | Self::Invoke { request_id, .. } => request_id,
393        }
394    }
395
396    pub fn api_version(&self) -> u32 {
397        match self {
398            Self::Handshake { api_version, .. }
399            | Self::Health { api_version, .. }
400            | Self::Invoke { api_version, .. } => *api_version,
401        }
402    }
403}
404
405// ---------------------------------------------------------------------------
406// IPC v1 responses (frozen since 0.5.0)
407// ---------------------------------------------------------------------------
408
409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
410#[serde(
411    tag = "kind",
412    rename_all = "camelCase",
413    rename_all_fields = "camelCase",
414    deny_unknown_fields
415)]
416pub enum RuntimeResponse {
417    Handshake {
418        request_id: String,
419        api_version: u32,
420        runtime_version: String,
421        model_pack_id: String,
422        model_pack_version: String,
423        capabilities: Vec<String>,
424    },
425    Health {
426        request_id: String,
427        api_version: u32,
428        healthy: bool,
429        model_pack_id: String,
430        model_pack_version: String,
431    },
432    Result {
433        request_id: String,
434        api_version: u32,
435        output: serde_json::Value,
436    },
437    Error {
438        request_id: String,
439        api_version: u32,
440        code: String,
441        message: String,
442        retryable: bool,
443    },
444}
445
446// ---------------------------------------------------------------------------
447// IPC v2 responses (introduced in 0.7.0)
448// ---------------------------------------------------------------------------
449
450#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
451#[serde(
452    tag = "kind",
453    rename_all = "camelCase",
454    rename_all_fields = "camelCase",
455    deny_unknown_fields
456)]
457pub enum RuntimeResponseV2 {
458    Handshake {
459        request_id: String,
460        api_version: u32,
461        runtime_version: String,
462        model_pack_id: String,
463        model_pack_version: String,
464        capabilities: Vec<String>,
465        handler_id: String,
466        handler_version: String,
467        handler_api_version: u32,
468        effective_capabilities: Vec<String>,
469    },
470    Health {
471        request_id: String,
472        api_version: u32,
473        healthy: bool,
474        model_pack_id: String,
475        model_pack_version: String,
476    },
477    Result {
478        request_id: String,
479        api_version: u32,
480        output: serde_json::Value,
481    },
482    Error {
483        request_id: String,
484        api_version: u32,
485        code: String,
486        message: String,
487        retryable: bool,
488    },
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn protocol_v1_roundtrip_is_tagged_and_strict() {
497        let request = RuntimeRequest::Health {
498            request_id: "health-1".into(),
499            api_version: 1,
500        };
501        let json = serde_json::to_string(&request).unwrap();
502        assert!(json.contains("\"method\":\"health\""));
503        let restored: RuntimeRequest = serde_json::from_str(&json).unwrap();
504        assert_eq!(restored, request);
505        assert!(
506            serde_json::from_str::<RuntimeRequest>(
507                r#"{"method":"health","requestId":"x","apiVersion":1,"extra":true}"#
508            )
509            .is_err()
510        );
511    }
512
513    #[test]
514    fn v1_handshake_fixture_is_stable() {
515        let request = RuntimeRequest::Handshake {
516            request_id: "fixture".into(),
517            api_version: 1,
518            client_name: "example-host".into(),
519            client_version: "0.6.10".into(),
520        };
521        assert_eq!(
522            serde_json::to_string(&request).unwrap(),
523            r#"{"method":"handshake","requestId":"fixture","apiVersion":1,"clientName":"example-host","clientVersion":"0.6.10"}"#
524        );
525    }
526
527    #[test]
528    fn v1_handshake_response_fixture_is_stable() {
529        let response = RuntimeResponse::Handshake {
530            request_id: "fixture".into(),
531            api_version: 1,
532            runtime_version: "0.6.0".into(),
533            model_pack_id: "rillml.example.default".into(),
534            model_pack_version: "0.6.0".into(),
535            capabilities: vec!["rillml.example".into()],
536        };
537        assert_eq!(
538            serde_json::to_string(&response).unwrap(),
539            r#"{"kind":"handshake","requestId":"fixture","apiVersion":1,"runtimeVersion":"0.6.0","modelPackId":"rillml.example.default","modelPackVersion":"0.6.0","capabilities":["rillml.example"]}"#
540        );
541    }
542
543    #[test]
544    fn v2_handshake_response_fixture_is_stable() {
545        let response = RuntimeResponseV2::Handshake {
546            request_id: "v2-fixture".into(),
547            api_version: 2,
548            runtime_version: "0.7.0".into(),
549            model_pack_id: "rillml.example.default".into(),
550            model_pack_version: "0.7.0".into(),
551            capabilities: vec!["rillml.example".into()],
552            handler_id: "org.example.handler".into(),
553            handler_version: "1.0.0".into(),
554            handler_api_version: 1,
555            effective_capabilities: vec!["rillml.example".into()],
556        };
557        let json = serde_json::to_string(&response).unwrap();
558        assert!(json.contains("\"handlerId\":\"org.example.handler\""));
559        assert!(json.contains("\"handlerApiVersion\":1"));
560        assert!(json.contains("\"effectiveCapabilities\":[\"rillml.example\"]"));
561        // Mutating the response produces a different JSON, proving the fixture
562        // is fully serialised and not relying on default values.
563        let mut bad = serde_json::from_str::<RuntimeResponseV2>(&json).unwrap();
564        if let RuntimeResponseV2::Handshake { handler_id, .. } = &mut bad {
565            handler_id.push('x');
566        }
567        let bad_json = serde_json::to_string(&bad).unwrap();
568        assert_ne!(bad_json, json);
569    }
570
571    #[test]
572    fn v1_response_rejects_handler_fields() {
573        let json = r#"{"kind":"handshake","requestId":"x","apiVersion":1,"runtimeVersion":"0.7.0","modelPackId":"m","modelPackVersion":"1","capabilities":["c"],"handlerId":"h"}"#;
574        assert!(serde_json::from_str::<RuntimeResponse>(json).is_err());
575    }
576
577    #[test]
578    fn invoke_roundtrip_preserves_capability_and_input() {
579        let request = RuntimeRequest::Invoke {
580            request_id: "invoke-1".into(),
581            api_version: 2,
582            capability: "rillml.example".into(),
583            input: serde_json::json!({"samples": []}),
584        };
585        let json = serde_json::to_string(&request).unwrap();
586        assert!(json.contains("\"method\":\"invoke\""));
587        assert!(json.contains("\"capability\":\"rillml.example\""));
588        let restored: RuntimeRequest = serde_json::from_str(&json).unwrap();
589        assert_eq!(restored, request);
590    }
591
592    #[test]
593    fn release_artifacts_enforce_platform_boundaries() {
594        let runtime = ReleaseArtifact {
595            kind: ReleaseArtifactKind::Runtime,
596            id: RUNTIME_ARTIFACT_ID.into(),
597            version: "0.7.0".into(),
598            runtime_api_version: RUNTIME_API_VERSION,
599            target_os: Some("macos".into()),
600            target_arch: Some("aarch64".into()),
601            handler_api_version: None,
602            min_runtime_version: None,
603            url: "https://example.invalid/rill-runtime".into(),
604            sha256: "ab".repeat(32),
605            size: 1024,
606        };
607        assert!(runtime.validate_shape().is_ok());
608
609        let mut model = runtime.clone();
610        model.kind = ReleaseArtifactKind::Model;
611        model.id = "rillml.example.default".into();
612        model.target_os = None;
613        model.target_arch = None;
614        assert!(model.validate_shape().is_ok());
615
616        let mut handler = runtime.clone();
617        handler.kind = ReleaseArtifactKind::Handler;
618        handler.id = "org.example.handler".into();
619        handler.target_os = None;
620        handler.target_arch = None;
621        handler.handler_api_version = Some(HANDLER_API_VERSION);
622        handler.min_runtime_version = Some("0.7.0".into());
623        assert!(handler.validate_shape().is_ok());
624
625        // Handler with platform fields is rejected.
626        handler.target_os = Some("linux".into());
627        assert!(handler.validate_shape().is_err());
628        handler.target_os = None;
629
630        // Handler without handler_api_version is rejected.
631        handler.handler_api_version = None;
632        assert!(handler.validate_shape().is_err());
633        handler.handler_api_version = Some(HANDLER_API_VERSION);
634
635        // Handler without min_runtime_version is rejected.
636        handler.min_runtime_version = None;
637        assert!(handler.validate_shape().is_err());
638    }
639
640    #[test]
641    fn handler_manifest_validates_shape() {
642        let manifest = HandlerPackManifest {
643            format_version: HANDLER_PACKAGE_FORMAT_VERSION,
644            id: "org.example.handler".into(),
645            version: "1.0.0".into(),
646            handler_api_version: HANDLER_API_VERSION,
647            min_runtime_version: "0.7.0".into(),
648            publisher_key_id: "test-key".into(),
649            capabilities: vec!["org.example.predict".into()],
650            module_sha256: "ab".repeat(32),
651            module_size: 1024,
652        };
653        assert!(manifest.validate_shape().is_ok());
654
655        let mut bad = manifest.clone();
656        bad.format_version = 99;
657        assert!(bad.validate_shape().is_err());
658
659        let mut bad = manifest.clone();
660        bad.handler_api_version = 99;
661        assert!(bad.validate_shape().is_err());
662
663        let mut bad = manifest.clone();
664        bad.capabilities = vec![];
665        assert!(bad.validate_shape().is_err());
666
667        let mut bad = manifest.clone();
668        bad.module_sha256 = "short".into();
669        assert!(bad.validate_shape().is_err());
670
671        let mut bad = manifest.clone();
672        bad.module_size = 0;
673        assert!(bad.validate_shape().is_err());
674    }
675}