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;
35pub const MAX_MESSAGE_BYTES: usize = 1024 * 1024;
37
38pub const RUNTIME_ARTIFACT_ID: &str = "rill-runtime";
39
40pub mod error_code {
56 pub const INVALID_JSON: &str = "invalidJson";
58 pub const INVALID_REQUEST_ID: &str = "invalidRequestId";
60 pub const INCOMPATIBLE_API_VERSION: &str = "incompatibleApiVersion";
62 pub const INVALID_CLIENT_IDENTITY: &str = "invalidClientIdentity";
64 pub const UNSUPPORTED_CAPABILITY: &str = "unsupportedCapability";
66 pub const NO_INVOKE_HANDLER: &str = "noInvokeHandler";
68 pub const HANDLER_TIMEOUT: &str = "handlerTimeout";
70 pub const HANDLER_TRAP: &str = "handlerTrap";
72 pub const HANDLER_OUTPUT_TOO_LARGE: &str = "handlerOutputTooLarge";
74 pub const HANDLER_INVALID_OUTPUT: &str = "handlerInvalidOutput";
76 pub const HANDLER_INTERNAL_ERROR: &str = "handlerInternalError";
79
80 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 pub fn is_frozen(code: &str) -> bool {
101 FROZEN_CODES.contains(&code)
102 }
103}
104
105#[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#[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#[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 pub signature: String,
359}
360
361#[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#[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#[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 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.target_os = Some("linux".into());
631 assert!(handler.validate_shape().is_err());
632 handler.target_os = None;
633
634 handler.handler_api_version = None;
636 assert!(handler.validate_shape().is_err());
637 handler.handler_api_version = Some(HANDLER_API_VERSION);
638
639 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}