1use std::sync::Arc;
2
3use rill_runtime_protocol::{
4 MIN_RUNTIME_API_VERSION, RUNTIME_API_VERSION, RuntimeRequest, RuntimeResponse,
5 RuntimeResponseV2,
6};
7use serde_json::Value;
8
9use crate::handler::HandlerIdentity;
10use crate::package::LoadedModelPack;
11
12pub trait InvokeHandler: Send + Sync + std::fmt::Debug {
14 fn invoke(&self, capability: &str, input: &Value) -> Result<Value, String>;
15}
16
17#[derive(Debug, Clone)]
21pub enum EngineResponse {
22 Handshake {
23 request_id: String,
24 runtime_version: String,
25 model_pack_id: String,
26 model_pack_version: String,
27 capabilities: Vec<String>,
28 handler: Option<HandlerIdentity>,
29 },
30 Health {
31 request_id: String,
32 healthy: bool,
33 model_pack_id: String,
34 model_pack_version: String,
35 },
36 Result {
37 request_id: String,
38 output: Value,
39 },
40 Error {
41 request_id: String,
42 code: String,
43 message: String,
44 retryable: bool,
45 },
46}
47
48impl EngineResponse {
49 pub fn to_v1(&self, api_version: u32) -> RuntimeResponse {
51 match self {
52 Self::Handshake {
53 request_id,
54 runtime_version,
55 model_pack_id,
56 model_pack_version,
57 capabilities,
58 ..
59 } => RuntimeResponse::Handshake {
60 request_id: request_id.clone(),
61 api_version,
62 runtime_version: runtime_version.clone(),
63 model_pack_id: model_pack_id.clone(),
64 model_pack_version: model_pack_version.clone(),
65 capabilities: capabilities.clone(),
66 },
67 Self::Health {
68 request_id,
69 healthy,
70 model_pack_id,
71 model_pack_version,
72 } => RuntimeResponse::Health {
73 request_id: request_id.clone(),
74 api_version,
75 healthy: *healthy,
76 model_pack_id: model_pack_id.clone(),
77 model_pack_version: model_pack_version.clone(),
78 },
79 Self::Result { request_id, output } => RuntimeResponse::Result {
80 request_id: request_id.clone(),
81 api_version,
82 output: output.clone(),
83 },
84 Self::Error {
85 request_id,
86 code,
87 message,
88 retryable,
89 } => RuntimeResponse::Error {
90 request_id: request_id.clone(),
91 api_version,
92 code: code.clone(),
93 message: message.clone(),
94 retryable: *retryable,
95 },
96 }
97 }
98
99 pub fn to_v2(&self, api_version: u32) -> RuntimeResponseV2 {
103 match self {
104 Self::Handshake {
105 request_id,
106 runtime_version,
107 model_pack_id,
108 model_pack_version,
109 capabilities,
110 handler,
111 } => {
112 let (handler_id, handler_version, handler_api_version, effective) = match handler {
113 Some(h) => (
114 h.handler_id.clone(),
115 h.handler_version.clone(),
116 h.handler_api_version,
117 h.effective_capabilities.clone(),
118 ),
119 None => (String::new(), String::new(), 0, capabilities.clone()),
120 };
121 RuntimeResponseV2::Handshake {
122 request_id: request_id.clone(),
123 api_version,
124 runtime_version: runtime_version.clone(),
125 model_pack_id: model_pack_id.clone(),
126 model_pack_version: model_pack_version.clone(),
127 capabilities: capabilities.clone(),
128 handler_id,
129 handler_version,
130 handler_api_version,
131 effective_capabilities: effective,
132 }
133 }
134 Self::Health {
135 request_id,
136 healthy,
137 model_pack_id,
138 model_pack_version,
139 } => RuntimeResponseV2::Health {
140 request_id: request_id.clone(),
141 api_version,
142 healthy: *healthy,
143 model_pack_id: model_pack_id.clone(),
144 model_pack_version: model_pack_version.clone(),
145 },
146 Self::Result { request_id, output } => RuntimeResponseV2::Result {
147 request_id: request_id.clone(),
148 api_version,
149 output: output.clone(),
150 },
151 Self::Error {
152 request_id,
153 code,
154 message,
155 retryable,
156 } => RuntimeResponseV2::Error {
157 request_id: request_id.clone(),
158 api_version,
159 code: code.clone(),
160 message: message.clone(),
161 retryable: *retryable,
162 },
163 }
164 }
165}
166
167#[derive(Debug, Clone)]
168pub struct RuntimeEngine {
169 pack: LoadedModelPack,
170 invoke_handler: Option<Arc<dyn InvokeHandler>>,
171 handler_identity: Option<HandlerIdentity>,
172 effective_capabilities: Vec<String>,
173}
174
175impl RuntimeEngine {
176 pub fn new(pack: LoadedModelPack) -> Self {
177 Self {
178 pack,
179 invoke_handler: None,
180 handler_identity: None,
181 effective_capabilities: Vec::new(),
182 }
183 }
184
185 pub fn with_invoke_handler(mut self, handler: Arc<dyn InvokeHandler>) -> Self {
186 self.invoke_handler = Some(handler);
187 self
188 }
189
190 pub fn with_handler_identity(mut self, identity: HandlerIdentity) -> Self {
192 self.effective_capabilities = identity.effective_capabilities.clone();
193 self.handler_identity = Some(identity);
194 self
195 }
196
197 pub fn effective_capabilities(&self) -> &[String] {
200 &self.effective_capabilities
201 }
202
203 pub fn handler_identity(&self) -> Option<&HandlerIdentity> {
205 self.handler_identity.as_ref()
206 }
207
208 pub fn handle(&self, request: RuntimeRequest) -> EngineResponse {
209 let request_id = request.request_id().to_string();
210 if request_id.is_empty() || request_id.len() > 128 {
211 return self.error(request_id, "invalidRequestId", "invalid request id", false);
212 }
213 let api_version = request.api_version();
214 if !(MIN_RUNTIME_API_VERSION..=RUNTIME_API_VERSION).contains(&api_version) {
215 return self.error(
216 request_id,
217 "incompatibleApiVersion",
218 "runtime API version is not supported",
219 false,
220 );
221 }
222
223 match request {
224 RuntimeRequest::Handshake {
225 request_id,
226 client_name,
227 client_version,
228 ..
229 } => {
230 if client_name.is_empty()
231 || client_name.len() > 96
232 || client_version.is_empty()
233 || client_version.len() > 48
234 {
235 return self.error(
236 request_id,
237 "invalidClientIdentity",
238 "invalid client identity",
239 false,
240 );
241 }
242 EngineResponse::Handshake {
243 request_id,
244 runtime_version: env!("CARGO_PKG_VERSION").into(),
245 model_pack_id: self.pack.manifest.id.clone(),
246 model_pack_version: self.pack.manifest.version.clone(),
247 capabilities: self.pack.manifest.capabilities.clone(),
248 handler: self.handler_identity.clone(),
249 }
250 }
251 RuntimeRequest::Health { request_id, .. } => EngineResponse::Health {
252 request_id,
253 healthy: true,
254 model_pack_id: self.pack.manifest.id.clone(),
255 model_pack_version: self.pack.manifest.version.clone(),
256 },
257 RuntimeRequest::Invoke {
258 request_id,
259 capability,
260 input,
261 ..
262 } => {
263 if !self.is_capability_allowed(&capability) {
264 return self.error(
265 request_id,
266 "unsupportedCapability",
267 "capability is not in the effective set",
268 false,
269 );
270 }
271 let Some(handler) = &self.invoke_handler else {
272 return self.error(
273 request_id,
274 "noInvokeHandler",
275 "no invoke handler registered",
276 false,
277 );
278 };
279 match handler.invoke(&capability, &input) {
280 Ok(output) => EngineResponse::Result { request_id, output },
281 Err(message) => {
282 let (code, retryable) = map_invoke_error(&message);
283 self.error(request_id, code, &message, retryable)
284 }
285 }
286 }
287 }
288 }
289
290 fn is_capability_allowed(&self, capability: &str) -> bool {
295 if !self.effective_capabilities.is_empty() {
296 self.effective_capabilities.iter().any(|c| c == capability)
297 } else {
298 self.pack
299 .manifest
300 .capabilities
301 .iter()
302 .any(|c| c == capability)
303 }
304 }
305
306 fn error(
307 &self,
308 request_id: String,
309 code: &str,
310 message: &str,
311 retryable: bool,
312 ) -> EngineResponse {
313 EngineResponse::Error {
314 request_id,
315 code: code.into(),
316 message: message.into(),
317 retryable,
318 }
319 }
320}
321
322fn map_invoke_error(message: &str) -> (&'static str, bool) {
325 if message.starts_with("handlerTrap") {
326 ("handlerTrap", false)
327 } else if message.starts_with("handlerTimeout") {
328 ("handlerTimeout", true)
329 } else if message.starts_with("handlerOutputTooLarge") {
330 ("handlerOutputTooLarge", false)
331 } else if message.starts_with("handlerInvalidOutput") {
332 ("handlerInvalidOutput", false)
333 } else if message.starts_with("handlerCapabilityMismatch") {
334 ("handlerCapabilityMismatch", false)
335 } else {
336 ("invokeFailed", false)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use rill_runtime_protocol::{MODEL_PACK_FORMAT_VERSION, ModelPackManifest};
343
344 use super::*;
345 use crate::handler::builtin::LINEAR_REGRESSION_CAPABILITY;
346
347 fn engine() -> RuntimeEngine {
348 RuntimeEngine::new(LoadedModelPack {
349 manifest: ModelPackManifest {
350 format_version: MODEL_PACK_FORMAT_VERSION,
351 id: "rillml.example.default".into(),
352 version: "0.7.0".into(),
353 runtime_api_version: RUNTIME_API_VERSION,
354 min_runtime_version: "0.7.0".into(),
355 publisher_key_id: "test".into(),
356 capabilities: vec!["rillml.example".into()],
357 },
358 model: serde_json::json!({}),
359 })
360 }
361
362 #[test]
363 fn handshake_reports_loaded_pack() {
364 let response = engine().handle(RuntimeRequest::Handshake {
365 request_id: "hello".into(),
366 api_version: RUNTIME_API_VERSION,
367 client_name: "example-host".into(),
368 client_version: "0.9.0".into(),
369 });
370 assert!(matches!(
371 response,
372 EngineResponse::Handshake { model_pack_id, .. }
373 if model_pack_id == "rillml.example.default"
374 ));
375 }
376
377 #[test]
378 fn incompatible_api_is_a_typed_error() {
379 let response = engine().handle(RuntimeRequest::Health {
380 request_id: "health".into(),
381 api_version: RUNTIME_API_VERSION + 1,
382 });
383 assert!(matches!(
384 response,
385 EngineResponse::Error { code, .. } if code == "incompatibleApiVersion"
386 ));
387 }
388
389 #[test]
390 fn invoke_without_handler_returns_no_invoke_handler_error() {
391 let response = engine().handle(RuntimeRequest::Invoke {
392 request_id: "invoke-1".into(),
393 api_version: RUNTIME_API_VERSION,
394 capability: "rillml.example".into(),
395 input: serde_json::json!({}),
396 });
397 assert!(matches!(
398 response,
399 EngineResponse::Error { code, .. } if code == "noInvokeHandler"
400 ));
401 }
402
403 #[test]
404 fn invoke_rejects_capability_not_declared_by_signed_manifest() {
405 let response = engine().handle(RuntimeRequest::Invoke {
406 request_id: "invoke-undeclared".into(),
407 api_version: RUNTIME_API_VERSION,
408 capability: "undeclared.capability".into(),
409 input: serde_json::json!({}),
410 });
411 assert!(matches!(
412 response,
413 EngineResponse::Error { code, .. } if code == "unsupportedCapability"
414 ));
415 }
416
417 #[test]
418 fn v1_handshake_omits_handler_fields() {
419 let identity = HandlerIdentity {
420 handler_id: "org.example.handler".into(),
421 handler_version: "1.0.0".into(),
422 handler_api_version: 1,
423 effective_capabilities: vec!["rillml.example".into()],
424 };
425 let engine = engine().with_handler_identity(identity);
426 let response = engine.handle(RuntimeRequest::Handshake {
427 request_id: "v1-test".into(),
428 api_version: 1,
429 client_name: "v1-host".into(),
430 client_version: "0.6.0".into(),
431 });
432 let v1 = response.to_v1(1);
433 let json = serde_json::to_string(&v1).unwrap();
434 assert!(!json.contains("handlerId"));
435 assert!(!json.contains("effectiveCapabilities"));
436 }
437
438 #[test]
439 fn v2_handshake_includes_handler_fields() {
440 let identity = HandlerIdentity {
441 handler_id: "org.example.handler".into(),
442 handler_version: "1.0.0".into(),
443 handler_api_version: 1,
444 effective_capabilities: vec!["rillml.example".into()],
445 };
446 let engine = engine().with_handler_identity(identity);
447 let response = engine.handle(RuntimeRequest::Handshake {
448 request_id: "v2-test".into(),
449 api_version: 2,
450 client_name: "v2-host".into(),
451 client_version: "0.7.0".into(),
452 });
453 let v2 = response.to_v2(2);
454 let json = serde_json::to_string(&v2).unwrap();
455 assert!(json.contains("\"handlerId\":\"org.example.handler\""));
456 assert!(json.contains("\"handlerApiVersion\":1"));
457 assert!(json.contains("\"effectiveCapabilities\":[\"rillml.example\"]"));
458 }
459
460 #[test]
461 fn v2_handshake_without_handler_has_empty_fields() {
462 let response = engine().handle(RuntimeRequest::Handshake {
463 request_id: "v2-no-handler".into(),
464 api_version: 2,
465 client_name: "v2-host".into(),
466 client_version: "0.7.0".into(),
467 });
468 let v2 = response.to_v2(2);
469 match v2 {
470 RuntimeResponseV2::Handshake {
471 handler_id,
472 handler_version,
473 handler_api_version,
474 effective_capabilities,
475 ..
476 } => {
477 assert!(handler_id.is_empty());
478 assert!(handler_version.is_empty());
479 assert_eq!(handler_api_version, 0);
480 assert_eq!(effective_capabilities, vec!["rillml.example"]);
481 }
482 _ => panic!("expected handshake"),
483 }
484 }
485
486 #[test]
487 fn linear_regression_handler_validates_and_predicts() {
488 use crate::handler::builtin::LinearRegressionInvokeHandler;
489
490 let pack = LoadedModelPack {
491 manifest: ModelPackManifest {
492 format_version: MODEL_PACK_FORMAT_VERSION,
493 id: "rillml.example.default".into(),
494 version: "0.7.0".into(),
495 runtime_api_version: RUNTIME_API_VERSION,
496 min_runtime_version: "0.7.0".into(),
497 publisher_key_id: "test".into(),
498 capabilities: vec![LINEAR_REGRESSION_CAPABILITY.into()],
499 },
500 model: serde_json::json!({
501 "kind": "linearRegression",
502 "weights": [0.5, -0.25],
503 "intercept": 1.0
504 }),
505 };
506 let handler = LinearRegressionInvokeHandler::from_pack(&pack).unwrap();
507 let engine = RuntimeEngine::new(pack).with_invoke_handler(Arc::new(handler));
508 let response = engine.handle(RuntimeRequest::Invoke {
509 request_id: "invoke-linear".into(),
510 api_version: RUNTIME_API_VERSION,
511 capability: LINEAR_REGRESSION_CAPABILITY.into(),
512 input: serde_json::json!({"features": [4.0, 2.0]}),
513 });
514 assert!(matches!(
515 response,
516 EngineResponse::Result { output, .. } if output["prediction"] == 2.5
517 ));
518 }
519}