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