1use serde::{Deserialize, Serialize};
16
17pub mod v3;
20
21pub const MIN_RUNTIME_API_VERSION: u32 = 1;
23pub const RUNTIME_API_VERSION: u32 = 2;
25pub const MODEL_PACK_FORMAT_VERSION: u32 = 1;
27pub const HANDLER_PACKAGE_FORMAT_VERSION: u32 = 1;
29pub const HANDLER_API_VERSION: u32 = 1;
31pub const RUNTIME_STATE_FORMAT_VERSION: u32 = 1;
33pub const RELEASE_INDEX_SCHEMA_VERSION: u32 = 3;
43pub const TRUST_METADATA_SCHEMA_VERSION: u32 = 1;
47pub const RELEASE_INDEX_LIFECYCLE_SCHEMA_VERSION: u32 = 1;
49pub const MAX_MESSAGE_BYTES: usize = 1024 * 1024;
51
52pub const RUNTIME_ARTIFACT_ID: &str = "rill-runtime";
54pub const RUNTIME_ARTIFACT_ID_MUSL: &str = "rill-runtime-musl";
58pub const PM_ADAPTER_ARTIFACT_ID: &str = "rill-pm-adapter";
63pub const PM_ADAPTER_PROTOCOL_VERSION: u32 = 1;
66
67pub mod error_code {
83 pub const INVALID_JSON: &str = "invalidJson";
85 pub const INVALID_REQUEST_ID: &str = "invalidRequestId";
87 pub const INCOMPATIBLE_API_VERSION: &str = "incompatibleApiVersion";
89 pub const INVALID_CLIENT_IDENTITY: &str = "invalidClientIdentity";
91 pub const UNSUPPORTED_CAPABILITY: &str = "unsupportedCapability";
93 pub const NO_INVOKE_HANDLER: &str = "noInvokeHandler";
95 pub const HANDLER_TIMEOUT: &str = "handlerTimeout";
97 pub const HANDLER_TRAP: &str = "handlerTrap";
99 pub const HANDLER_OUTPUT_TOO_LARGE: &str = "handlerOutputTooLarge";
101 pub const HANDLER_INVALID_OUTPUT: &str = "handlerInvalidOutput";
103 pub const HANDLER_INTERNAL_ERROR: &str = "handlerInternalError";
106
107 pub const FROZEN_CODES: &[&str] = &[
113 HANDLER_INTERNAL_ERROR,
114 HANDLER_INVALID_OUTPUT,
115 HANDLER_OUTPUT_TOO_LARGE,
116 HANDLER_TIMEOUT,
117 HANDLER_TRAP,
118 INCOMPATIBLE_API_VERSION,
119 INVALID_CLIENT_IDENTITY,
120 INVALID_JSON,
121 INVALID_REQUEST_ID,
122 NO_INVOKE_HANDLER,
123 UNSUPPORTED_CAPABILITY,
124 ];
125
126 pub fn is_frozen(code: &str) -> bool {
128 FROZEN_CODES.contains(&code)
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137#[serde(rename_all = "camelCase", deny_unknown_fields)]
138pub struct ModelPackManifest {
139 pub format_version: u32,
140 pub id: String,
141 pub version: String,
142 pub runtime_api_version: u32,
143 pub min_runtime_version: String,
144 pub publisher_key_id: String,
145 pub capabilities: Vec<String>,
146}
147
148impl ModelPackManifest {
149 pub fn validate_shape(&self) -> Result<(), &'static str> {
150 if self.format_version != MODEL_PACK_FORMAT_VERSION {
151 return Err("unsupported model-pack format version");
152 }
153 if self.runtime_api_version != RUNTIME_API_VERSION {
154 return Err("unsupported runtime API version");
155 }
156 if self.id.is_empty() || self.id.len() > 96 {
157 return Err("invalid model-pack id");
158 }
159 if self.version.is_empty() || self.version.len() > 48 {
160 return Err("invalid model-pack version");
161 }
162 if self.publisher_key_id.is_empty() || self.publisher_key_id.len() > 96 {
163 return Err("invalid publisher key id");
164 }
165 Self::validate_capabilities(&self.capabilities)?;
166 Ok(())
167 }
168
169 pub fn validate_capabilities(capabilities: &[String]) -> Result<(), &'static str> {
170 if capabilities.is_empty() || capabilities.len() > 32 {
171 return Err("invalid capabilities list");
172 }
173 if capabilities
174 .iter()
175 .any(|capability| capability.is_empty() || capability.len() > 96)
176 {
177 return Err("invalid capability string");
178 }
179 let mut seen = std::collections::HashSet::new();
180 if !capabilities
181 .iter()
182 .all(|capability| seen.insert(capability.clone()))
183 {
184 return Err("duplicate capability");
185 }
186 Ok(())
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
195#[serde(rename_all = "camelCase", deny_unknown_fields)]
196pub struct HandlerPackManifest {
197 pub format_version: u32,
198 pub id: String,
199 pub version: String,
200 pub handler_api_version: u32,
201 pub min_runtime_version: String,
202 pub publisher_key_id: String,
203 pub capabilities: Vec<String>,
204 pub module_sha256: String,
205 pub module_size: u64,
206}
207
208impl HandlerPackManifest {
209 pub fn validate_shape(&self) -> Result<(), &'static str> {
210 if self.format_version != HANDLER_PACKAGE_FORMAT_VERSION {
211 return Err("unsupported handler-pack format version");
212 }
213 if self.handler_api_version != HANDLER_API_VERSION {
214 return Err("unsupported handler API version");
215 }
216 if self.id.is_empty() || self.id.len() > 96 {
217 return Err("invalid handler id");
218 }
219 if self.version.is_empty() || self.version.len() > 48 {
220 return Err("invalid handler version");
221 }
222 if self.publisher_key_id.is_empty() || self.publisher_key_id.len() > 96 {
223 return Err("invalid handler publisher key id");
224 }
225 if self.min_runtime_version.is_empty() || self.min_runtime_version.len() > 48 {
226 return Err("invalid minimum runtime version");
227 }
228 ModelPackManifest::validate_capabilities(&self.capabilities)?;
229 if self.module_sha256.len() != 64
230 || !self
231 .module_sha256
232 .bytes()
233 .all(|byte| byte.is_ascii_hexdigit())
234 {
235 return Err("invalid module SHA-256");
236 }
237 if self.module_size == 0 || self.module_size > 4 * 1024 * 1024 {
238 return Err("invalid module size");
239 }
240 Ok(())
241 }
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
249#[serde(rename_all = "camelCase")]
250pub enum ReleaseArtifactKind {
251 Runtime,
252 Model,
253 Handler,
254 #[serde(rename = "pm-adapter")]
255 PmAdapter,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
259#[serde(rename_all = "camelCase", deny_unknown_fields)]
260pub struct ReleaseArtifact {
261 pub kind: ReleaseArtifactKind,
262 pub id: String,
263 pub version: String,
264 #[serde(default)]
265 pub runtime_api_version: u32,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub target_os: Option<String>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub target_arch: Option<String>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub target_libc: Option<String>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub handler_api_version: Option<u32>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub min_runtime_version: Option<String>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub pm_adapter_protocol_version: Option<u32>,
282 pub url: String,
283 pub sha256: String,
284 pub size: u64,
285}
286
287impl ReleaseArtifact {
288 pub fn validate_shape(&self) -> Result<(), &'static str> {
289 if self.id.is_empty() || self.id.len() > 96 {
290 return Err("invalid artifact id");
291 }
292 if self.version.is_empty() || self.version.len() > 48 {
293 return Err("invalid artifact version");
294 }
295 if self.url.is_empty() || self.url.len() > 2048 {
296 return Err("invalid artifact URL");
297 }
298 if self.sha256.len() != 64 || !self.sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
299 return Err("invalid artifact SHA-256");
300 }
301 if self.size == 0 || self.size > 128 * 1024 * 1024 {
302 return Err("invalid artifact size");
303 }
304 match self.kind {
305 ReleaseArtifactKind::Runtime => {
306 if self.runtime_api_version != RUNTIME_API_VERSION {
307 return Err("unsupported artifact runtime API version");
308 }
309 if (self.id != RUNTIME_ARTIFACT_ID && self.id != RUNTIME_ARTIFACT_ID_MUSL)
315 || self.target_os.as_deref().is_none_or(str::is_empty)
316 || self.target_arch.as_deref().is_none_or(str::is_empty)
317 {
318 return Err("runtime artifact requires a target OS and architecture");
319 }
320 match self.target_os.as_deref() {
323 Some("linux") => {
324 let libc = self.target_libc.as_deref();
325 let expected = if self.id == RUNTIME_ARTIFACT_ID_MUSL {
326 Some("musl")
327 } else {
328 Some("gnu")
329 };
330 if libc != expected {
331 return Err("runtime artifact libc variant does not match its id");
332 }
333 }
334 _ => {
335 if self.target_libc.is_some() {
336 return Err("non-Linux runtime artifact must not carry a libc variant");
337 }
338 }
339 }
340 if self.handler_api_version.is_some() || self.min_runtime_version.is_some() {
341 return Err("runtime artifact must not carry handler fields");
342 }
343 }
344 ReleaseArtifactKind::Model => {
345 if self.runtime_api_version != RUNTIME_API_VERSION {
346 return Err("unsupported artifact runtime API version");
347 }
348 if self.target_os.is_some()
349 || self.target_arch.is_some()
350 || self.handler_api_version.is_some()
351 || self.min_runtime_version.is_some()
352 {
353 return Err("model artifact must be platform independent");
354 }
355 }
356 ReleaseArtifactKind::Handler => {
357 if self.runtime_api_version != RUNTIME_API_VERSION {
358 return Err("unsupported artifact runtime API version");
359 }
360 if self.target_os.is_some() || self.target_arch.is_some() {
361 return Err("handler artifact must be platform independent");
362 }
363 let handler_api = self
364 .handler_api_version
365 .ok_or("handler artifact requires handler API version")?;
366 if handler_api != HANDLER_API_VERSION {
367 return Err("unsupported handler API version");
368 }
369 let min_runtime = self
370 .min_runtime_version
371 .as_deref()
372 .ok_or("handler artifact requires minimum runtime version")?;
373 if min_runtime.is_empty() || min_runtime.len() > 48 {
374 return Err("invalid minimum runtime version");
375 }
376 }
377 ReleaseArtifactKind::PmAdapter => {
378 if self.runtime_api_version != 0 {
383 return Err("pm-adapter artifact must not set runtime API version");
384 }
385 if self.id != PM_ADAPTER_ARTIFACT_ID
386 || self.target_os.as_deref().is_none_or(str::is_empty)
387 || self.target_arch.as_deref().is_none_or(str::is_empty)
388 {
389 return Err("pm-adapter artifact requires a target OS and architecture");
390 }
391 if self.handler_api_version.is_some() || self.min_runtime_version.is_some() {
392 return Err("pm-adapter artifact must not carry handler fields");
393 }
394 if self.pm_adapter_protocol_version != Some(PM_ADAPTER_PROTOCOL_VERSION) {
395 return Err("unsupported pm-adapter protocol version");
396 }
397 }
398 }
399 Ok(())
400 }
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
404#[serde(rename_all = "camelCase", deny_unknown_fields)]
405pub struct ReleaseIndexPayload {
406 pub schema_version: u32,
407 pub channel: String,
408 pub generated_at: String,
409 pub publisher_key_id: String,
410 pub artifacts: Vec<ReleaseArtifact>,
411}
412
413impl ReleaseIndexPayload {
414 pub fn validate_shape(&self) -> Result<(), &'static str> {
415 if self.schema_version != RELEASE_INDEX_SCHEMA_VERSION {
416 return Err("unsupported release-index schema");
417 }
418 if !matches!(self.channel.as_str(), "stable" | "candidate") {
419 return Err("unsupported release channel");
420 }
421 if self.generated_at.is_empty() || self.generated_at.len() > 64 {
422 return Err("invalid release-index timestamp");
423 }
424 if self.publisher_key_id.is_empty() || self.publisher_key_id.len() > 96 {
425 return Err("invalid release-index publisher");
426 }
427 if self.artifacts.is_empty() || self.artifacts.len() > 64 {
428 return Err("invalid release-index artifact count");
429 }
430 for artifact in &self.artifacts {
431 artifact.validate_shape()?;
432 }
433 Ok(())
434 }
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
438#[serde(rename_all = "camelCase", deny_unknown_fields)]
439pub struct SignedReleaseIndex {
440 pub payload: ReleaseIndexPayload,
441 pub signature: String,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
447#[serde(rename_all = "camelCase", deny_unknown_fields)]
448pub struct TrustKeyMetadataV1 {
449 pub key_id: String,
450 pub public_key_hex: String,
451 pub role: TrustKeyRole,
452 pub not_before_unix_ms: u64,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub not_after_unix_ms: Option<u64>,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub revoked_at_unix_ms: Option<u64>,
457 #[serde(default)]
458 pub emergency_revoked: bool,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
463#[serde(rename_all = "camelCase")]
464pub enum TrustKeyRole {
465 Current,
466 Next,
467}
468
469impl TrustKeyMetadataV1 {
470 pub fn validate_shape(&self) -> Result<(), &'static str> {
471 if self.key_id.is_empty() || self.key_id.len() > 96 {
472 return Err("invalid trust key id");
473 }
474 if self.public_key_hex.len() != 64
475 || !self
476 .public_key_hex
477 .bytes()
478 .all(|byte| byte.is_ascii_hexdigit())
479 {
480 return Err("invalid trust public key");
481 }
482 if let Some(not_after) = self.not_after_unix_ms
483 && not_after <= self.not_before_unix_ms
484 {
485 return Err("trust key validity window is empty");
486 }
487 Ok(())
488 }
489
490 pub fn is_active_at(&self, now_unix_ms: u64) -> bool {
492 self.not_before_unix_ms <= now_unix_ms
493 && self
494 .not_after_unix_ms
495 .is_none_or(|not_after| now_unix_ms < not_after)
496 && self
497 .revoked_at_unix_ms
498 .is_none_or(|revoked_at| now_unix_ms < revoked_at)
499 && !self.emergency_revoked
500 }
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
510#[serde(rename_all = "camelCase", deny_unknown_fields)]
511pub struct TrustMetadataV1 {
512 pub schema_version: u32,
513 pub metadata_generation: u64,
514 pub minimum_release_generation: u64,
515 pub keys: Vec<TrustKeyMetadataV1>,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
526#[serde(rename_all = "camelCase", deny_unknown_fields)]
527pub struct TrustVerificationFloorV1 {
528 pub minimum_metadata_generation: u64,
529 pub minimum_release_generation: u64,
530 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub metadata_digest: Option<String>,
532}
533
534impl TrustMetadataV1 {
535 pub fn validate_shape(&self) -> Result<(), &'static str> {
536 if self.schema_version != TRUST_METADATA_SCHEMA_VERSION {
537 return Err("unsupported trust metadata schema");
538 }
539 if self.keys.is_empty() || self.keys.len() > 64 {
540 return Err("invalid trust key count");
541 }
542 let mut ids = std::collections::BTreeSet::new();
543 for key in &self.keys {
544 key.validate_shape()?;
545 if !ids.insert(&key.key_id) {
546 return Err("duplicate trust key id");
547 }
548 }
549 if !self
550 .keys
551 .iter()
552 .any(|key| matches!(&key.role, TrustKeyRole::Current))
553 {
554 return Err("trust metadata has no current key");
555 }
556 Ok(())
557 }
558
559 pub fn active_keys_at(
561 &self,
562 now_unix_ms: u64,
563 ) -> Result<Vec<&TrustKeyMetadataV1>, &'static str> {
564 self.validate_shape()?;
565 let active: Vec<_> = self
566 .keys
567 .iter()
568 .filter(|key| key.is_active_at(now_unix_ms))
569 .collect();
570 if !active
571 .iter()
572 .any(|key| matches!(&key.role, TrustKeyRole::Current))
573 {
574 return Err("trust metadata has no active current key");
575 }
576 Ok(active)
577 }
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
582#[serde(rename_all = "camelCase", deny_unknown_fields)]
583pub struct SignedReleaseIndexWithGenerationV1 {
584 pub schema_version: u32,
585 pub release_generation: u64,
586 pub index: SignedReleaseIndex,
587 pub lifecycle_signature: String,
590}
591
592impl SignedReleaseIndexWithGenerationV1 {
593 pub fn validate_shape(&self) -> Result<(), &'static str> {
594 if self.schema_version != RELEASE_INDEX_LIFECYCLE_SCHEMA_VERSION {
595 return Err("unsupported release lifecycle schema");
596 }
597 if self.lifecycle_signature.len() != 128
598 || !self
599 .lifecycle_signature
600 .bytes()
601 .all(|byte| byte.is_ascii_hexdigit())
602 {
603 return Err("invalid release lifecycle signature");
604 }
605 Ok(())
606 }
607}
608
609#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
614#[serde(
615 tag = "method",
616 rename_all = "camelCase",
617 rename_all_fields = "camelCase",
618 deny_unknown_fields
619)]
620pub enum RuntimeRequest {
621 Handshake {
622 request_id: String,
623 api_version: u32,
624 client_name: String,
625 client_version: String,
626 },
627 Health {
628 request_id: String,
629 api_version: u32,
630 },
631 Invoke {
632 request_id: String,
633 api_version: u32,
634 capability: String,
635 input: serde_json::Value,
636 },
637}
638
639impl RuntimeRequest {
640 pub fn request_id(&self) -> &str {
641 match self {
642 Self::Handshake { request_id, .. }
643 | Self::Health { request_id, .. }
644 | Self::Invoke { request_id, .. } => request_id,
645 }
646 }
647
648 pub fn api_version(&self) -> u32 {
649 match self {
650 Self::Handshake { api_version, .. }
651 | Self::Health { api_version, .. }
652 | Self::Invoke { api_version, .. } => *api_version,
653 }
654 }
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
662#[serde(
663 tag = "kind",
664 rename_all = "camelCase",
665 rename_all_fields = "camelCase",
666 deny_unknown_fields
667)]
668pub enum RuntimeResponse {
669 Handshake {
670 request_id: String,
671 api_version: u32,
672 runtime_version: String,
673 model_pack_id: String,
674 model_pack_version: String,
675 capabilities: Vec<String>,
676 },
677 Health {
678 request_id: String,
679 api_version: u32,
680 healthy: bool,
681 model_pack_id: String,
682 model_pack_version: String,
683 },
684 Result {
685 request_id: String,
686 api_version: u32,
687 output: serde_json::Value,
688 },
689 Error {
690 request_id: String,
691 api_version: u32,
692 code: String,
693 message: String,
694 retryable: bool,
695 },
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
703#[serde(
704 tag = "kind",
705 rename_all = "camelCase",
706 rename_all_fields = "camelCase",
707 deny_unknown_fields
708)]
709pub enum RuntimeResponseV2 {
710 Handshake {
711 request_id: String,
712 api_version: u32,
713 runtime_version: String,
714 model_pack_id: String,
715 model_pack_version: String,
716 capabilities: Vec<String>,
717 handler_id: String,
718 handler_version: String,
719 handler_api_version: u32,
720 effective_capabilities: Vec<String>,
721 },
722 Health {
723 request_id: String,
724 api_version: u32,
725 healthy: bool,
726 model_pack_id: String,
727 model_pack_version: String,
728 },
729 Result {
730 request_id: String,
731 api_version: u32,
732 output: serde_json::Value,
733 },
734 Error {
735 request_id: String,
736 api_version: u32,
737 code: String,
738 message: String,
739 retryable: bool,
740 },
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746
747 #[test]
748 fn protocol_v1_roundtrip_is_tagged_and_strict() {
749 let request = RuntimeRequest::Health {
750 request_id: "health-1".into(),
751 api_version: 1,
752 };
753 let json = serde_json::to_string(&request).unwrap();
754 assert!(json.contains("\"method\":\"health\""));
755 let restored: RuntimeRequest = serde_json::from_str(&json).unwrap();
756 assert_eq!(restored, request);
757 assert!(
758 serde_json::from_str::<RuntimeRequest>(
759 r#"{"method":"health","requestId":"x","apiVersion":1,"extra":true}"#
760 )
761 .is_err()
762 );
763 }
764
765 #[test]
766 fn v1_handshake_fixture_is_stable() {
767 let request = RuntimeRequest::Handshake {
768 request_id: "fixture".into(),
769 api_version: 1,
770 client_name: "example-host".into(),
771 client_version: "0.6.10".into(),
772 };
773 assert_eq!(
774 serde_json::to_string(&request).unwrap(),
775 r#"{"method":"handshake","requestId":"fixture","apiVersion":1,"clientName":"example-host","clientVersion":"0.6.10"}"#
776 );
777 }
778
779 #[test]
780 fn v1_handshake_response_fixture_is_stable() {
781 let response = RuntimeResponse::Handshake {
782 request_id: "fixture".into(),
783 api_version: 1,
784 runtime_version: "0.6.0".into(),
785 model_pack_id: "rillml.example.default".into(),
786 model_pack_version: "0.6.0".into(),
787 capabilities: vec!["rillml.example".into()],
788 };
789 assert_eq!(
790 serde_json::to_string(&response).unwrap(),
791 r#"{"kind":"handshake","requestId":"fixture","apiVersion":1,"runtimeVersion":"0.6.0","modelPackId":"rillml.example.default","modelPackVersion":"0.6.0","capabilities":["rillml.example"]}"#
792 );
793 }
794
795 #[test]
796 fn v2_handshake_response_fixture_is_stable() {
797 let response = RuntimeResponseV2::Handshake {
798 request_id: "v2-fixture".into(),
799 api_version: 2,
800 runtime_version: "0.7.0".into(),
801 model_pack_id: "rillml.example.default".into(),
802 model_pack_version: "0.7.0".into(),
803 capabilities: vec!["rillml.example".into()],
804 handler_id: "org.example.handler".into(),
805 handler_version: "1.0.0".into(),
806 handler_api_version: 1,
807 effective_capabilities: vec!["rillml.example".into()],
808 };
809 let json = serde_json::to_string(&response).unwrap();
810 assert!(json.contains("\"handlerId\":\"org.example.handler\""));
811 assert!(json.contains("\"handlerApiVersion\":1"));
812 assert!(json.contains("\"effectiveCapabilities\":[\"rillml.example\"]"));
813 let mut bad = serde_json::from_str::<RuntimeResponseV2>(&json).unwrap();
816 if let RuntimeResponseV2::Handshake { handler_id, .. } = &mut bad {
817 handler_id.push('x');
818 }
819 let bad_json = serde_json::to_string(&bad).unwrap();
820 assert_ne!(bad_json, json);
821 }
822
823 #[test]
824 fn v1_response_rejects_handler_fields() {
825 let json = r#"{"kind":"handshake","requestId":"x","apiVersion":1,"runtimeVersion":"0.7.0","modelPackId":"m","modelPackVersion":"1","capabilities":["c"],"handlerId":"h"}"#;
826 assert!(serde_json::from_str::<RuntimeResponse>(json).is_err());
827 }
828
829 #[test]
830 fn invoke_roundtrip_preserves_capability_and_input() {
831 let request = RuntimeRequest::Invoke {
832 request_id: "invoke-1".into(),
833 api_version: 2,
834 capability: "rillml.example".into(),
835 input: serde_json::json!({"samples": []}),
836 };
837 let json = serde_json::to_string(&request).unwrap();
838 assert!(json.contains("\"method\":\"invoke\""));
839 assert!(json.contains("\"capability\":\"rillml.example\""));
840 let restored: RuntimeRequest = serde_json::from_str(&json).unwrap();
841 assert_eq!(restored, request);
842 }
843
844 #[test]
845 fn release_artifacts_enforce_platform_boundaries() {
846 let runtime = ReleaseArtifact {
847 kind: ReleaseArtifactKind::Runtime,
848 id: RUNTIME_ARTIFACT_ID.into(),
849 version: "0.7.0".into(),
850 runtime_api_version: RUNTIME_API_VERSION,
851 target_os: Some("macos".into()),
852 target_arch: Some("aarch64".into()),
853 target_libc: None,
854 handler_api_version: None,
855 min_runtime_version: None,
856 pm_adapter_protocol_version: None,
857 url: "https://example.invalid/rill-runtime".into(),
858 sha256: "ab".repeat(32),
859 size: 1024,
860 };
861 assert!(runtime.validate_shape().is_ok());
862
863 let mut model = runtime.clone();
864 model.kind = ReleaseArtifactKind::Model;
865 model.id = "rillml.example.default".into();
866 model.target_os = None;
867 model.target_arch = None;
868 assert!(model.validate_shape().is_ok());
869
870 let mut handler = runtime.clone();
871 handler.kind = ReleaseArtifactKind::Handler;
872 handler.id = "org.example.handler".into();
873 handler.target_os = None;
874 handler.target_arch = None;
875 handler.handler_api_version = Some(HANDLER_API_VERSION);
876 handler.min_runtime_version = Some("0.7.0".into());
877 assert!(handler.validate_shape().is_ok());
878
879 handler.target_os = Some("linux".into());
881 assert!(handler.validate_shape().is_err());
882 handler.target_os = None;
883
884 handler.handler_api_version = None;
886 assert!(handler.validate_shape().is_err());
887 handler.handler_api_version = Some(HANDLER_API_VERSION);
888
889 handler.min_runtime_version = None;
891 assert!(handler.validate_shape().is_err());
892 }
893
894 #[test]
895 fn pm_adapter_artifact_roundtrip_and_shape_validation() {
896 let json = r#"{"kind":"pm-adapter","id":"rill-pm-adapter","version":"1.2.0-rc.1","pmAdapterProtocolVersion":1,"targetOs":"linux","targetArch":"x86_64","url":"https://example.invalid/adapter","sha256":"3333333333333333333333333333333333333333333333333333333333333333","size":4096}"#;
899 let artifact: ReleaseArtifact = serde_json::from_str(json).unwrap();
900 assert_eq!(artifact.kind, ReleaseArtifactKind::PmAdapter);
901 assert_eq!(
902 artifact.pm_adapter_protocol_version,
903 Some(PM_ADAPTER_PROTOCOL_VERSION)
904 );
905 assert!(artifact.validate_shape().is_ok());
906
907 assert!(
909 serde_json::from_str::<ReleaseArtifact>(&json.replace("pm-adapter", "pmAdapter"))
910 .is_err()
911 );
912
913 let mut bad = artifact.clone();
915 bad.pm_adapter_protocol_version = Some(99);
916 assert!(bad.validate_shape().is_err());
917
918 let mut bad = artifact.clone();
920 bad.handler_api_version = Some(HANDLER_API_VERSION);
921 assert!(bad.validate_shape().is_err());
922
923 let mut bad = artifact.clone();
925 bad.runtime_api_version = RUNTIME_API_VERSION;
926 assert!(bad.validate_shape().is_err());
927
928 let mut bad = artifact.clone();
930 bad.target_os = None;
931 assert!(bad.validate_shape().is_err());
932
933 let mut bad = artifact.clone();
935 bad.id = "org.example.other".into();
936 assert!(bad.validate_shape().is_err());
937 }
938
939 #[test]
940 fn handler_manifest_validates_shape() {
941 let manifest = HandlerPackManifest {
942 format_version: HANDLER_PACKAGE_FORMAT_VERSION,
943 id: "org.example.handler".into(),
944 version: "1.0.0".into(),
945 handler_api_version: HANDLER_API_VERSION,
946 min_runtime_version: "0.7.0".into(),
947 publisher_key_id: "test-key".into(),
948 capabilities: vec!["org.example.predict".into()],
949 module_sha256: "ab".repeat(32),
950 module_size: 1024,
951 };
952 assert!(manifest.validate_shape().is_ok());
953
954 let mut bad = manifest.clone();
955 bad.format_version = 99;
956 assert!(bad.validate_shape().is_err());
957
958 let mut bad = manifest.clone();
959 bad.handler_api_version = 99;
960 assert!(bad.validate_shape().is_err());
961
962 let mut bad = manifest.clone();
963 bad.capabilities = vec![];
964 assert!(bad.validate_shape().is_err());
965
966 let mut bad = manifest.clone();
967 bad.module_sha256 = "short".into();
968 assert!(bad.validate_shape().is_err());
969
970 let mut bad = manifest.clone();
971 bad.module_size = 0;
972 assert!(bad.validate_shape().is_err());
973 }
974}