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