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 = 2;
40pub const MAX_MESSAGE_BYTES: usize = 1024 * 1024;
42
43pub const RUNTIME_ARTIFACT_ID: &str = "rill-runtime";
45pub const RUNTIME_ARTIFACT_ID_MUSL: &str = "rill-runtime-musl";
49
50pub mod error_code {
66 pub const INVALID_JSON: &str = "invalidJson";
68 pub const INVALID_REQUEST_ID: &str = "invalidRequestId";
70 pub const INCOMPATIBLE_API_VERSION: &str = "incompatibleApiVersion";
72 pub const INVALID_CLIENT_IDENTITY: &str = "invalidClientIdentity";
74 pub const UNSUPPORTED_CAPABILITY: &str = "unsupportedCapability";
76 pub const NO_INVOKE_HANDLER: &str = "noInvokeHandler";
78 pub const HANDLER_TIMEOUT: &str = "handlerTimeout";
80 pub const HANDLER_TRAP: &str = "handlerTrap";
82 pub const HANDLER_OUTPUT_TOO_LARGE: &str = "handlerOutputTooLarge";
84 pub const HANDLER_INVALID_OUTPUT: &str = "handlerInvalidOutput";
86 pub const HANDLER_INTERNAL_ERROR: &str = "handlerInternalError";
89
90 pub const FROZEN_CODES: &[&str] = &[
96 HANDLER_INTERNAL_ERROR,
97 HANDLER_INVALID_OUTPUT,
98 HANDLER_OUTPUT_TOO_LARGE,
99 HANDLER_TIMEOUT,
100 HANDLER_TRAP,
101 INCOMPATIBLE_API_VERSION,
102 INVALID_CLIENT_IDENTITY,
103 INVALID_JSON,
104 INVALID_REQUEST_ID,
105 NO_INVOKE_HANDLER,
106 UNSUPPORTED_CAPABILITY,
107 ];
108
109 pub fn is_frozen(code: &str) -> bool {
111 FROZEN_CODES.contains(&code)
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
120#[serde(rename_all = "camelCase", deny_unknown_fields)]
121pub struct ModelPackManifest {
122 pub format_version: u32,
123 pub id: String,
124 pub version: String,
125 pub runtime_api_version: u32,
126 pub min_runtime_version: String,
127 pub publisher_key_id: String,
128 pub capabilities: Vec<String>,
129}
130
131impl ModelPackManifest {
132 pub fn validate_shape(&self) -> Result<(), &'static str> {
133 if self.format_version != MODEL_PACK_FORMAT_VERSION {
134 return Err("unsupported model-pack format version");
135 }
136 if self.runtime_api_version != RUNTIME_API_VERSION {
137 return Err("unsupported runtime API version");
138 }
139 if self.id.is_empty() || self.id.len() > 96 {
140 return Err("invalid model-pack id");
141 }
142 if self.version.is_empty() || self.version.len() > 48 {
143 return Err("invalid model-pack version");
144 }
145 if self.publisher_key_id.is_empty() || self.publisher_key_id.len() > 96 {
146 return Err("invalid publisher key id");
147 }
148 Self::validate_capabilities(&self.capabilities)?;
149 Ok(())
150 }
151
152 pub fn validate_capabilities(capabilities: &[String]) -> Result<(), &'static str> {
153 if capabilities.is_empty() || capabilities.len() > 32 {
154 return Err("invalid capabilities list");
155 }
156 if capabilities
157 .iter()
158 .any(|capability| capability.is_empty() || capability.len() > 96)
159 {
160 return Err("invalid capability string");
161 }
162 let mut seen = std::collections::HashSet::new();
163 if !capabilities
164 .iter()
165 .all(|capability| seen.insert(capability.clone()))
166 {
167 return Err("duplicate capability");
168 }
169 Ok(())
170 }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
178#[serde(rename_all = "camelCase", deny_unknown_fields)]
179pub struct HandlerPackManifest {
180 pub format_version: u32,
181 pub id: String,
182 pub version: String,
183 pub handler_api_version: u32,
184 pub min_runtime_version: String,
185 pub publisher_key_id: String,
186 pub capabilities: Vec<String>,
187 pub module_sha256: String,
188 pub module_size: u64,
189}
190
191impl HandlerPackManifest {
192 pub fn validate_shape(&self) -> Result<(), &'static str> {
193 if self.format_version != HANDLER_PACKAGE_FORMAT_VERSION {
194 return Err("unsupported handler-pack format version");
195 }
196 if self.handler_api_version != HANDLER_API_VERSION {
197 return Err("unsupported handler API version");
198 }
199 if self.id.is_empty() || self.id.len() > 96 {
200 return Err("invalid handler id");
201 }
202 if self.version.is_empty() || self.version.len() > 48 {
203 return Err("invalid handler version");
204 }
205 if self.publisher_key_id.is_empty() || self.publisher_key_id.len() > 96 {
206 return Err("invalid handler publisher key id");
207 }
208 if self.min_runtime_version.is_empty() || self.min_runtime_version.len() > 48 {
209 return Err("invalid minimum runtime version");
210 }
211 ModelPackManifest::validate_capabilities(&self.capabilities)?;
212 if self.module_sha256.len() != 64
213 || !self
214 .module_sha256
215 .bytes()
216 .all(|byte| byte.is_ascii_hexdigit())
217 {
218 return Err("invalid module SHA-256");
219 }
220 if self.module_size == 0 || self.module_size > 4 * 1024 * 1024 {
221 return Err("invalid module size");
222 }
223 Ok(())
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
232#[serde(rename_all = "camelCase")]
233pub enum ReleaseArtifactKind {
234 Runtime,
235 Model,
236 Handler,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
240#[serde(rename_all = "camelCase", deny_unknown_fields)]
241pub struct ReleaseArtifact {
242 pub kind: ReleaseArtifactKind,
243 pub id: String,
244 pub version: String,
245 pub runtime_api_version: u32,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub target_os: Option<String>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub target_arch: Option<String>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub handler_api_version: Option<u32>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub min_runtime_version: Option<String>,
254 pub url: String,
255 pub sha256: String,
256 pub size: u64,
257}
258
259impl ReleaseArtifact {
260 pub fn validate_shape(&self) -> Result<(), &'static str> {
261 if self.id.is_empty() || self.id.len() > 96 {
262 return Err("invalid artifact id");
263 }
264 if self.version.is_empty() || self.version.len() > 48 {
265 return Err("invalid artifact version");
266 }
267 if self.url.is_empty() || self.url.len() > 2048 {
268 return Err("invalid artifact URL");
269 }
270 if self.sha256.len() != 64 || !self.sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
271 return Err("invalid artifact SHA-256");
272 }
273 if self.size == 0 || self.size > 128 * 1024 * 1024 {
274 return Err("invalid artifact size");
275 }
276 match self.kind {
277 ReleaseArtifactKind::Runtime => {
278 if self.runtime_api_version != RUNTIME_API_VERSION {
279 return Err("unsupported artifact runtime API version");
280 }
281 if (self.id != RUNTIME_ARTIFACT_ID && self.id != RUNTIME_ARTIFACT_ID_MUSL)
286 || self.target_os.as_deref().is_none_or(str::is_empty)
287 || self.target_arch.as_deref().is_none_or(str::is_empty)
288 {
289 return Err("runtime artifact requires a target OS and architecture");
290 }
291 if self.handler_api_version.is_some() || self.min_runtime_version.is_some() {
292 return Err("runtime artifact must not carry handler fields");
293 }
294 }
295 ReleaseArtifactKind::Model => {
296 if self.runtime_api_version != RUNTIME_API_VERSION {
297 return Err("unsupported artifact runtime API version");
298 }
299 if self.target_os.is_some()
300 || self.target_arch.is_some()
301 || self.handler_api_version.is_some()
302 || self.min_runtime_version.is_some()
303 {
304 return Err("model artifact must be platform independent");
305 }
306 }
307 ReleaseArtifactKind::Handler => {
308 if self.runtime_api_version != RUNTIME_API_VERSION {
309 return Err("unsupported artifact runtime API version");
310 }
311 if self.target_os.is_some() || self.target_arch.is_some() {
312 return Err("handler artifact must be platform independent");
313 }
314 let handler_api = self
315 .handler_api_version
316 .ok_or("handler artifact requires handler API version")?;
317 if handler_api != HANDLER_API_VERSION {
318 return Err("unsupported handler API version");
319 }
320 let min_runtime = self
321 .min_runtime_version
322 .as_deref()
323 .ok_or("handler artifact requires minimum runtime version")?;
324 if min_runtime.is_empty() || min_runtime.len() > 48 {
325 return Err("invalid minimum runtime version");
326 }
327 }
328 }
329 Ok(())
330 }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
334#[serde(rename_all = "camelCase", deny_unknown_fields)]
335pub struct ReleaseIndexPayload {
336 pub schema_version: u32,
337 pub channel: String,
338 pub generated_at: String,
339 pub publisher_key_id: String,
340 pub artifacts: Vec<ReleaseArtifact>,
341}
342
343impl ReleaseIndexPayload {
344 pub fn validate_shape(&self) -> Result<(), &'static str> {
345 if self.schema_version != RELEASE_INDEX_SCHEMA_VERSION {
346 return Err("unsupported release-index schema");
347 }
348 if !matches!(self.channel.as_str(), "stable" | "candidate") {
349 return Err("unsupported release channel");
350 }
351 if self.generated_at.is_empty() || self.generated_at.len() > 64 {
352 return Err("invalid release-index timestamp");
353 }
354 if self.publisher_key_id.is_empty() || self.publisher_key_id.len() > 96 {
355 return Err("invalid release-index publisher");
356 }
357 if self.artifacts.is_empty() || self.artifacts.len() > 64 {
358 return Err("invalid release-index artifact count");
359 }
360 for artifact in &self.artifacts {
361 artifact.validate_shape()?;
362 }
363 Ok(())
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
368#[serde(rename_all = "camelCase", deny_unknown_fields)]
369pub struct SignedReleaseIndex {
370 pub payload: ReleaseIndexPayload,
371 pub signature: String,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
380#[serde(
381 tag = "method",
382 rename_all = "camelCase",
383 rename_all_fields = "camelCase",
384 deny_unknown_fields
385)]
386pub enum RuntimeRequest {
387 Handshake {
388 request_id: String,
389 api_version: u32,
390 client_name: String,
391 client_version: String,
392 },
393 Health {
394 request_id: String,
395 api_version: u32,
396 },
397 Invoke {
398 request_id: String,
399 api_version: u32,
400 capability: String,
401 input: serde_json::Value,
402 },
403}
404
405impl RuntimeRequest {
406 pub fn request_id(&self) -> &str {
407 match self {
408 Self::Handshake { request_id, .. }
409 | Self::Health { request_id, .. }
410 | Self::Invoke { request_id, .. } => request_id,
411 }
412 }
413
414 pub fn api_version(&self) -> u32 {
415 match self {
416 Self::Handshake { api_version, .. }
417 | Self::Health { api_version, .. }
418 | Self::Invoke { api_version, .. } => *api_version,
419 }
420 }
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
428#[serde(
429 tag = "kind",
430 rename_all = "camelCase",
431 rename_all_fields = "camelCase",
432 deny_unknown_fields
433)]
434pub enum RuntimeResponse {
435 Handshake {
436 request_id: String,
437 api_version: u32,
438 runtime_version: String,
439 model_pack_id: String,
440 model_pack_version: String,
441 capabilities: Vec<String>,
442 },
443 Health {
444 request_id: String,
445 api_version: u32,
446 healthy: bool,
447 model_pack_id: String,
448 model_pack_version: String,
449 },
450 Result {
451 request_id: String,
452 api_version: u32,
453 output: serde_json::Value,
454 },
455 Error {
456 request_id: String,
457 api_version: u32,
458 code: String,
459 message: String,
460 retryable: bool,
461 },
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
469#[serde(
470 tag = "kind",
471 rename_all = "camelCase",
472 rename_all_fields = "camelCase",
473 deny_unknown_fields
474)]
475pub enum RuntimeResponseV2 {
476 Handshake {
477 request_id: String,
478 api_version: u32,
479 runtime_version: String,
480 model_pack_id: String,
481 model_pack_version: String,
482 capabilities: Vec<String>,
483 handler_id: String,
484 handler_version: String,
485 handler_api_version: u32,
486 effective_capabilities: Vec<String>,
487 },
488 Health {
489 request_id: String,
490 api_version: u32,
491 healthy: bool,
492 model_pack_id: String,
493 model_pack_version: String,
494 },
495 Result {
496 request_id: String,
497 api_version: u32,
498 output: serde_json::Value,
499 },
500 Error {
501 request_id: String,
502 api_version: u32,
503 code: String,
504 message: String,
505 retryable: bool,
506 },
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn protocol_v1_roundtrip_is_tagged_and_strict() {
515 let request = RuntimeRequest::Health {
516 request_id: "health-1".into(),
517 api_version: 1,
518 };
519 let json = serde_json::to_string(&request).unwrap();
520 assert!(json.contains("\"method\":\"health\""));
521 let restored: RuntimeRequest = serde_json::from_str(&json).unwrap();
522 assert_eq!(restored, request);
523 assert!(
524 serde_json::from_str::<RuntimeRequest>(
525 r#"{"method":"health","requestId":"x","apiVersion":1,"extra":true}"#
526 )
527 .is_err()
528 );
529 }
530
531 #[test]
532 fn v1_handshake_fixture_is_stable() {
533 let request = RuntimeRequest::Handshake {
534 request_id: "fixture".into(),
535 api_version: 1,
536 client_name: "example-host".into(),
537 client_version: "0.6.10".into(),
538 };
539 assert_eq!(
540 serde_json::to_string(&request).unwrap(),
541 r#"{"method":"handshake","requestId":"fixture","apiVersion":1,"clientName":"example-host","clientVersion":"0.6.10"}"#
542 );
543 }
544
545 #[test]
546 fn v1_handshake_response_fixture_is_stable() {
547 let response = RuntimeResponse::Handshake {
548 request_id: "fixture".into(),
549 api_version: 1,
550 runtime_version: "0.6.0".into(),
551 model_pack_id: "rillml.example.default".into(),
552 model_pack_version: "0.6.0".into(),
553 capabilities: vec!["rillml.example".into()],
554 };
555 assert_eq!(
556 serde_json::to_string(&response).unwrap(),
557 r#"{"kind":"handshake","requestId":"fixture","apiVersion":1,"runtimeVersion":"0.6.0","modelPackId":"rillml.example.default","modelPackVersion":"0.6.0","capabilities":["rillml.example"]}"#
558 );
559 }
560
561 #[test]
562 fn v2_handshake_response_fixture_is_stable() {
563 let response = RuntimeResponseV2::Handshake {
564 request_id: "v2-fixture".into(),
565 api_version: 2,
566 runtime_version: "0.7.0".into(),
567 model_pack_id: "rillml.example.default".into(),
568 model_pack_version: "0.7.0".into(),
569 capabilities: vec!["rillml.example".into()],
570 handler_id: "org.example.handler".into(),
571 handler_version: "1.0.0".into(),
572 handler_api_version: 1,
573 effective_capabilities: vec!["rillml.example".into()],
574 };
575 let json = serde_json::to_string(&response).unwrap();
576 assert!(json.contains("\"handlerId\":\"org.example.handler\""));
577 assert!(json.contains("\"handlerApiVersion\":1"));
578 assert!(json.contains("\"effectiveCapabilities\":[\"rillml.example\"]"));
579 let mut bad = serde_json::from_str::<RuntimeResponseV2>(&json).unwrap();
582 if let RuntimeResponseV2::Handshake { handler_id, .. } = &mut bad {
583 handler_id.push('x');
584 }
585 let bad_json = serde_json::to_string(&bad).unwrap();
586 assert_ne!(bad_json, json);
587 }
588
589 #[test]
590 fn v1_response_rejects_handler_fields() {
591 let json = r#"{"kind":"handshake","requestId":"x","apiVersion":1,"runtimeVersion":"0.7.0","modelPackId":"m","modelPackVersion":"1","capabilities":["c"],"handlerId":"h"}"#;
592 assert!(serde_json::from_str::<RuntimeResponse>(json).is_err());
593 }
594
595 #[test]
596 fn invoke_roundtrip_preserves_capability_and_input() {
597 let request = RuntimeRequest::Invoke {
598 request_id: "invoke-1".into(),
599 api_version: 2,
600 capability: "rillml.example".into(),
601 input: serde_json::json!({"samples": []}),
602 };
603 let json = serde_json::to_string(&request).unwrap();
604 assert!(json.contains("\"method\":\"invoke\""));
605 assert!(json.contains("\"capability\":\"rillml.example\""));
606 let restored: RuntimeRequest = serde_json::from_str(&json).unwrap();
607 assert_eq!(restored, request);
608 }
609
610 #[test]
611 fn release_artifacts_enforce_platform_boundaries() {
612 let runtime = ReleaseArtifact {
613 kind: ReleaseArtifactKind::Runtime,
614 id: RUNTIME_ARTIFACT_ID.into(),
615 version: "0.7.0".into(),
616 runtime_api_version: RUNTIME_API_VERSION,
617 target_os: Some("macos".into()),
618 target_arch: Some("aarch64".into()),
619 handler_api_version: None,
620 min_runtime_version: None,
621 url: "https://example.invalid/rill-runtime".into(),
622 sha256: "ab".repeat(32),
623 size: 1024,
624 };
625 assert!(runtime.validate_shape().is_ok());
626
627 let mut model = runtime.clone();
628 model.kind = ReleaseArtifactKind::Model;
629 model.id = "rillml.example.default".into();
630 model.target_os = None;
631 model.target_arch = None;
632 assert!(model.validate_shape().is_ok());
633
634 let mut handler = runtime.clone();
635 handler.kind = ReleaseArtifactKind::Handler;
636 handler.id = "org.example.handler".into();
637 handler.target_os = None;
638 handler.target_arch = None;
639 handler.handler_api_version = Some(HANDLER_API_VERSION);
640 handler.min_runtime_version = Some("0.7.0".into());
641 assert!(handler.validate_shape().is_ok());
642
643 handler.target_os = Some("linux".into());
645 assert!(handler.validate_shape().is_err());
646 handler.target_os = None;
647
648 handler.handler_api_version = None;
650 assert!(handler.validate_shape().is_err());
651 handler.handler_api_version = Some(HANDLER_API_VERSION);
652
653 handler.min_runtime_version = None;
655 assert!(handler.validate_shape().is_err());
656 }
657
658 #[test]
659 fn handler_manifest_validates_shape() {
660 let manifest = HandlerPackManifest {
661 format_version: HANDLER_PACKAGE_FORMAT_VERSION,
662 id: "org.example.handler".into(),
663 version: "1.0.0".into(),
664 handler_api_version: HANDLER_API_VERSION,
665 min_runtime_version: "0.7.0".into(),
666 publisher_key_id: "test-key".into(),
667 capabilities: vec!["org.example.predict".into()],
668 module_sha256: "ab".repeat(32),
669 module_size: 1024,
670 };
671 assert!(manifest.validate_shape().is_ok());
672
673 let mut bad = manifest.clone();
674 bad.format_version = 99;
675 assert!(bad.validate_shape().is_err());
676
677 let mut bad = manifest.clone();
678 bad.handler_api_version = 99;
679 assert!(bad.validate_shape().is_err());
680
681 let mut bad = manifest.clone();
682 bad.capabilities = vec![];
683 assert!(bad.validate_shape().is_err());
684
685 let mut bad = manifest.clone();
686 bad.module_sha256 = "short".into();
687 assert!(bad.validate_shape().is_err());
688
689 let mut bad = manifest.clone();
690 bad.module_size = 0;
691 assert!(bad.validate_shape().is_err());
692 }
693}