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