use std::sync::Arc;
use rill_runtime_protocol::{
MIN_RUNTIME_API_VERSION, RUNTIME_API_VERSION, RuntimeRequest, RuntimeResponse,
RuntimeResponseV2, error_code,
};
use serde_json::Value;
use crate::handler::HandlerIdentity;
use crate::package::LoadedModelPack;
#[derive(Debug, Clone)]
pub struct InvokeError {
kind: InvokeErrorKind,
detail: Option<String>,
}
pub const MAX_DETAIL_BYTES: usize = 4 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvokeErrorKind {
Internal,
Timeout,
Trap,
OutputTooLarge,
InvalidOutput,
InvalidModel,
InvalidInput,
UnsupportedCapability,
ExecutionFailed,
}
impl InvokeError {
pub const fn new(kind: InvokeErrorKind) -> Self {
Self { kind, detail: None }
}
pub fn with_detail(kind: InvokeErrorKind, detail: impl Into<String>) -> Self {
Self {
kind,
detail: Some(truncate_to_bytes(detail.into(), MAX_DETAIL_BYTES)),
}
}
pub const fn kind(&self) -> InvokeErrorKind {
self.kind
}
pub fn detail(&self) -> Option<&str> {
self.detail.as_deref()
}
pub const fn stable_code(&self) -> &'static str {
match self.kind {
InvokeErrorKind::Internal => error_code::HANDLER_INTERNAL_ERROR,
InvokeErrorKind::Timeout => error_code::HANDLER_TIMEOUT,
InvokeErrorKind::Trap => error_code::HANDLER_TRAP,
InvokeErrorKind::OutputTooLarge => error_code::HANDLER_OUTPUT_TOO_LARGE,
InvokeErrorKind::InvalidOutput => error_code::HANDLER_INVALID_OUTPUT,
InvokeErrorKind::InvalidModel
| InvokeErrorKind::InvalidInput
| InvokeErrorKind::UnsupportedCapability
| InvokeErrorKind::ExecutionFailed => error_code::HANDLER_INTERNAL_ERROR,
}
}
pub const fn public_message(&self) -> &'static str {
match self.kind {
InvokeErrorKind::Internal => "internal runtime error",
InvokeErrorKind::Timeout => "handler exceeded the wall-clock deadline",
InvokeErrorKind::Trap => "handler trapped",
InvokeErrorKind::OutputTooLarge => "handler output exceeded the size limit",
InvokeErrorKind::InvalidOutput => "handler output was not valid JSON",
InvokeErrorKind::InvalidModel => "handler rejected the model configuration",
InvokeErrorKind::InvalidInput => "handler rejected the input",
InvokeErrorKind::UnsupportedCapability => "handler does not support the capability",
InvokeErrorKind::ExecutionFailed => "handler execution failed",
}
}
pub const fn retryable(&self) -> bool {
matches!(self.kind, InvokeErrorKind::Timeout)
}
}
impl std::fmt::Display for InvokeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.detail {
Some(detail) => write!(f, "{}: {}", self.stable_code(), detail),
None => f.write_str(self.stable_code()),
}
}
}
impl std::error::Error for InvokeError {}
fn truncate_to_bytes(s: String, max_bytes: usize) -> String {
if s.len() <= max_bytes {
return s;
}
let mut end = max_bytes;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
let mut truncated = s;
truncated.truncate(end);
truncated
}
pub trait HostLogSink: Send + Sync + std::fmt::Debug {
fn emit(&self, message: &str);
}
#[derive(Debug, Default, Clone)]
pub struct StderrLogSink;
impl HostLogSink for StderrLogSink {
fn emit(&self, message: &str) {
eprintln!("{message}");
}
}
pub trait InvokeHandler: Send + Sync + std::fmt::Debug {
fn invoke(&self, capability: &str, input: &Value) -> Result<Value, InvokeError>;
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum EngineResponse {
Handshake {
request_id: String,
runtime_version: String,
model_pack_id: String,
model_pack_version: String,
capabilities: Vec<String>,
handler: Option<HandlerIdentity>,
},
Health {
request_id: String,
healthy: bool,
model_pack_id: String,
model_pack_version: String,
},
Result {
request_id: String,
output: Value,
},
Error {
request_id: String,
code: String,
message: String,
retryable: bool,
},
}
impl EngineResponse {
pub fn to_v1(&self, api_version: u32) -> RuntimeResponse {
match self {
Self::Handshake {
request_id,
runtime_version,
model_pack_id,
model_pack_version,
capabilities,
..
} => RuntimeResponse::Handshake {
request_id: request_id.clone(),
api_version,
runtime_version: runtime_version.clone(),
model_pack_id: model_pack_id.clone(),
model_pack_version: model_pack_version.clone(),
capabilities: capabilities.clone(),
},
Self::Health {
request_id,
healthy,
model_pack_id,
model_pack_version,
} => RuntimeResponse::Health {
request_id: request_id.clone(),
api_version,
healthy: *healthy,
model_pack_id: model_pack_id.clone(),
model_pack_version: model_pack_version.clone(),
},
Self::Result { request_id, output } => RuntimeResponse::Result {
request_id: request_id.clone(),
api_version,
output: output.clone(),
},
Self::Error {
request_id,
code,
message,
retryable,
} => RuntimeResponse::Error {
request_id: request_id.clone(),
api_version,
code: code.clone(),
message: message.clone(),
retryable: *retryable,
},
}
}
pub fn to_v2(&self, api_version: u32) -> RuntimeResponseV2 {
match self {
Self::Handshake {
request_id,
runtime_version,
model_pack_id,
model_pack_version,
capabilities,
handler,
} => {
let (handler_id, handler_version, handler_api_version, effective) = match handler {
Some(h) => (
h.handler_id.clone(),
h.handler_version.clone(),
h.handler_api_version,
h.effective_capabilities.clone(),
),
None => (String::new(), String::new(), 0, capabilities.clone()),
};
RuntimeResponseV2::Handshake {
request_id: request_id.clone(),
api_version,
runtime_version: runtime_version.clone(),
model_pack_id: model_pack_id.clone(),
model_pack_version: model_pack_version.clone(),
capabilities: capabilities.clone(),
handler_id,
handler_version,
handler_api_version,
effective_capabilities: effective,
}
}
Self::Health {
request_id,
healthy,
model_pack_id,
model_pack_version,
} => RuntimeResponseV2::Health {
request_id: request_id.clone(),
api_version,
healthy: *healthy,
model_pack_id: model_pack_id.clone(),
model_pack_version: model_pack_version.clone(),
},
Self::Result { request_id, output } => RuntimeResponseV2::Result {
request_id: request_id.clone(),
api_version,
output: output.clone(),
},
Self::Error {
request_id,
code,
message,
retryable,
} => RuntimeResponseV2::Error {
request_id: request_id.clone(),
api_version,
code: code.clone(),
message: message.clone(),
retryable: *retryable,
},
}
}
}
#[derive(Debug, Clone)]
pub struct RuntimeEngine {
pack: LoadedModelPack,
invoke_handler: Option<Arc<dyn InvokeHandler>>,
handler_identity: Option<HandlerIdentity>,
effective_capabilities: Vec<String>,
log_sink: Arc<dyn HostLogSink>,
}
impl RuntimeEngine {
pub fn new(pack: LoadedModelPack) -> Self {
Self {
pack,
invoke_handler: None,
handler_identity: None,
effective_capabilities: Vec::new(),
log_sink: Arc::new(StderrLogSink),
}
}
pub fn with_invoke_handler(mut self, handler: Arc<dyn InvokeHandler>) -> Self {
self.invoke_handler = Some(handler);
self
}
pub fn with_log_sink(mut self, sink: Arc<dyn HostLogSink>) -> Self {
self.log_sink = sink;
self
}
pub fn with_handler_identity(mut self, identity: HandlerIdentity) -> Self {
self.effective_capabilities = identity.effective_capabilities.clone();
self.handler_identity = Some(identity);
self
}
pub fn effective_capabilities(&self) -> &[String] {
&self.effective_capabilities
}
pub fn handler_identity(&self) -> Option<&HandlerIdentity> {
self.handler_identity.as_ref()
}
pub fn handle(&self, request: RuntimeRequest) -> EngineResponse {
let request_id = request.request_id().to_string();
if request_id.is_empty() || request_id.len() > 128 {
return self.error(
request_id,
error_code::INVALID_REQUEST_ID,
"invalid request id",
false,
);
}
let api_version = request.api_version();
if !(MIN_RUNTIME_API_VERSION..=RUNTIME_API_VERSION).contains(&api_version) {
return self.error(
request_id,
error_code::INCOMPATIBLE_API_VERSION,
"runtime API version is not supported",
false,
);
}
match request {
RuntimeRequest::Handshake {
request_id,
client_name,
client_version,
..
} => {
if client_name.is_empty()
|| client_name.len() > 96
|| client_version.is_empty()
|| client_version.len() > 48
{
return self.error(
request_id,
error_code::INVALID_CLIENT_IDENTITY,
"invalid client identity",
false,
);
}
EngineResponse::Handshake {
request_id,
runtime_version: env!("CARGO_PKG_VERSION").into(),
model_pack_id: self.pack.manifest.id.clone(),
model_pack_version: self.pack.manifest.version.clone(),
capabilities: self.pack.manifest.capabilities.clone(),
handler: self.handler_identity.clone(),
}
}
RuntimeRequest::Health { request_id, .. } => EngineResponse::Health {
request_id,
healthy: true,
model_pack_id: self.pack.manifest.id.clone(),
model_pack_version: self.pack.manifest.version.clone(),
},
RuntimeRequest::Invoke {
request_id,
capability,
input,
..
} => {
if !self.is_capability_allowed(&capability) {
return self.error(
request_id,
error_code::UNSUPPORTED_CAPABILITY,
"capability is not in the effective set",
false,
);
}
let Some(handler) = &self.invoke_handler else {
return self.error(
request_id,
error_code::NO_INVOKE_HANDLER,
"no invoke handler registered",
false,
);
};
match handler.invoke(&capability, &input) {
Ok(output) => EngineResponse::Result { request_id, output },
Err(invoke_err) => {
if let Some(detail) = invoke_err.detail() {
self.log_sink.emit(&format!(
"rill-runtime: invoke {} -> {} (detail: {})",
capability,
invoke_err.stable_code(),
detail
));
}
self.error(
request_id,
invoke_err.stable_code(),
invoke_err.public_message(),
invoke_err.retryable(),
)
}
}
}
}
}
fn is_capability_allowed(&self, capability: &str) -> bool {
if !self.effective_capabilities.is_empty() {
self.effective_capabilities.iter().any(|c| c == capability)
} else {
self.pack
.manifest
.capabilities
.iter()
.any(|c| c == capability)
}
}
fn error(
&self,
request_id: String,
code: &str,
message: &str,
retryable: bool,
) -> EngineResponse {
EngineResponse::Error {
request_id,
code: code.into(),
message: message.into(),
retryable,
}
}
}
#[cfg(test)]
mod tests {
use rill_runtime_protocol::{MODEL_PACK_FORMAT_VERSION, ModelPackManifest};
use std::sync::Mutex;
use super::*;
use crate::handler::builtin::LINEAR_REGRESSION_CAPABILITY;
#[derive(Debug, Default)]
pub(crate) struct CapturingLogSink {
messages: Mutex<Vec<String>>,
}
impl CapturingLogSink {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn messages(&self) -> Vec<String> {
self.messages
.lock()
.expect("CapturingLogSink poisoned")
.clone()
}
#[allow(dead_code)]
pub(crate) fn total_bytes(&self) -> usize {
self.messages
.lock()
.expect("CapturingLogSink poisoned")
.iter()
.map(String::len)
.sum()
}
#[allow(dead_code)]
pub(crate) fn clear(&self) {
self.messages
.lock()
.expect("CapturingLogSink poisoned")
.clear();
}
}
impl HostLogSink for CapturingLogSink {
fn emit(&self, message: &str) {
self.messages
.lock()
.expect("CapturingLogSink poisoned")
.push(message.to_string());
}
}
fn engine() -> RuntimeEngine {
RuntimeEngine::new(LoadedModelPack {
manifest: ModelPackManifest {
format_version: MODEL_PACK_FORMAT_VERSION,
id: "rillml.example.default".into(),
version: "0.7.0".into(),
runtime_api_version: RUNTIME_API_VERSION,
min_runtime_version: "0.7.0".into(),
publisher_key_id: "test".into(),
capabilities: vec!["rillml.example".into()],
},
model: serde_json::json!({}),
})
}
#[test]
fn handshake_reports_loaded_pack() {
let response = engine().handle(RuntimeRequest::Handshake {
request_id: "hello".into(),
api_version: RUNTIME_API_VERSION,
client_name: "example-host".into(),
client_version: "0.9.0".into(),
});
assert!(matches!(
response,
EngineResponse::Handshake { model_pack_id, .. }
if model_pack_id == "rillml.example.default"
));
}
#[test]
fn incompatible_api_is_a_typed_error() {
let response = engine().handle(RuntimeRequest::Health {
request_id: "health".into(),
api_version: RUNTIME_API_VERSION + 1,
});
assert!(matches!(
response,
EngineResponse::Error { code, .. } if code == "incompatibleApiVersion"
));
}
#[test]
fn invoke_without_handler_returns_no_invoke_handler_error() {
let response = engine().handle(RuntimeRequest::Invoke {
request_id: "invoke-1".into(),
api_version: RUNTIME_API_VERSION,
capability: "rillml.example".into(),
input: serde_json::json!({}),
});
assert!(matches!(
response,
EngineResponse::Error { code, .. } if code == "noInvokeHandler"
));
}
#[test]
fn invoke_rejects_capability_not_declared_by_signed_manifest() {
let response = engine().handle(RuntimeRequest::Invoke {
request_id: "invoke-undeclared".into(),
api_version: RUNTIME_API_VERSION,
capability: "undeclared.capability".into(),
input: serde_json::json!({}),
});
assert!(matches!(
response,
EngineResponse::Error { code, .. } if code == "unsupportedCapability"
));
}
#[test]
fn v1_handshake_omits_handler_fields() {
let identity = HandlerIdentity {
handler_id: "org.example.handler".into(),
handler_version: "1.0.0".into(),
handler_api_version: 1,
effective_capabilities: vec!["rillml.example".into()],
};
let engine = engine().with_handler_identity(identity);
let response = engine.handle(RuntimeRequest::Handshake {
request_id: "v1-test".into(),
api_version: 1,
client_name: "v1-host".into(),
client_version: "0.6.0".into(),
});
let v1 = response.to_v1(1);
let json = serde_json::to_string(&v1).unwrap();
assert!(!json.contains("handlerId"));
assert!(!json.contains("effectiveCapabilities"));
}
#[test]
fn v2_handshake_includes_handler_fields() {
let identity = HandlerIdentity {
handler_id: "org.example.handler".into(),
handler_version: "1.0.0".into(),
handler_api_version: 1,
effective_capabilities: vec!["rillml.example".into()],
};
let engine = engine().with_handler_identity(identity);
let response = engine.handle(RuntimeRequest::Handshake {
request_id: "v2-test".into(),
api_version: 2,
client_name: "v2-host".into(),
client_version: "0.7.0".into(),
});
let v2 = response.to_v2(2);
let json = serde_json::to_string(&v2).unwrap();
assert!(json.contains("\"handlerId\":\"org.example.handler\""));
assert!(json.contains("\"handlerApiVersion\":1"));
assert!(json.contains("\"effectiveCapabilities\":[\"rillml.example\"]"));
}
#[test]
fn v2_handshake_without_handler_has_empty_fields() {
let response = engine().handle(RuntimeRequest::Handshake {
request_id: "v2-no-handler".into(),
api_version: 2,
client_name: "v2-host".into(),
client_version: "0.7.0".into(),
});
let v2 = response.to_v2(2);
match v2 {
RuntimeResponseV2::Handshake {
handler_id,
handler_version,
handler_api_version,
effective_capabilities,
..
} => {
assert!(handler_id.is_empty());
assert!(handler_version.is_empty());
assert_eq!(handler_api_version, 0);
assert_eq!(effective_capabilities, vec!["rillml.example"]);
}
_ => panic!("expected handshake"),
}
}
#[test]
fn linear_regression_handler_validates_and_predicts() {
use crate::handler::builtin::LinearRegressionInvokeHandler;
let pack = LoadedModelPack {
manifest: ModelPackManifest {
format_version: MODEL_PACK_FORMAT_VERSION,
id: "rillml.example.default".into(),
version: "0.7.0".into(),
runtime_api_version: RUNTIME_API_VERSION,
min_runtime_version: "0.7.0".into(),
publisher_key_id: "test".into(),
capabilities: vec![LINEAR_REGRESSION_CAPABILITY.into()],
},
model: serde_json::json!({
"kind": "linearRegression",
"weights": [0.5, -0.25],
"intercept": 1.0
}),
};
let handler = LinearRegressionInvokeHandler::from_pack(&pack).unwrap();
let engine = RuntimeEngine::new(pack).with_invoke_handler(Arc::new(handler));
let response = engine.handle(RuntimeRequest::Invoke {
request_id: "invoke-linear".into(),
api_version: RUNTIME_API_VERSION,
capability: LINEAR_REGRESSION_CAPABILITY.into(),
input: serde_json::json!({"features": [4.0, 2.0]}),
});
assert!(matches!(
response,
EngineResponse::Result { output, .. } if output["prediction"] == 2.5
));
}
#[test]
fn invoke_error_stable_codes_match_wire_format() {
assert_eq!(
InvokeError::new(InvokeErrorKind::Trap).stable_code(),
"handlerTrap"
);
assert_eq!(
InvokeError::new(InvokeErrorKind::Timeout).stable_code(),
"handlerTimeout"
);
assert_eq!(
InvokeError::new(InvokeErrorKind::OutputTooLarge).stable_code(),
"handlerOutputTooLarge"
);
assert_eq!(
InvokeError::new(InvokeErrorKind::InvalidOutput).stable_code(),
"handlerInvalidOutput"
);
assert_eq!(
InvokeError::new(InvokeErrorKind::Internal).stable_code(),
"handlerInternalError"
);
for kind in [
InvokeErrorKind::InvalidModel,
InvokeErrorKind::InvalidInput,
InvokeErrorKind::UnsupportedCapability,
InvokeErrorKind::ExecutionFailed,
] {
assert_eq!(
InvokeError::new(kind).stable_code(),
"handlerInternalError",
"{kind:?} must map to handlerInternalError for v1/v2 compat"
);
}
}
#[test]
fn invoke_error_retryable_only_for_timeout() {
assert!(InvokeError::new(InvokeErrorKind::Timeout).retryable());
for kind in [
InvokeErrorKind::Trap,
InvokeErrorKind::OutputTooLarge,
InvokeErrorKind::InvalidOutput,
InvokeErrorKind::Internal,
InvokeErrorKind::InvalidModel,
InvokeErrorKind::InvalidInput,
InvokeErrorKind::UnsupportedCapability,
InvokeErrorKind::ExecutionFailed,
] {
assert!(
!InvokeError::new(kind).retryable(),
"{kind:?} must not be retryable"
);
}
}
#[test]
fn invoke_error_guest_variants_have_distinct_public_messages() {
let messages = [
InvokeError::new(InvokeErrorKind::InvalidModel).public_message(),
InvokeError::new(InvokeErrorKind::InvalidInput).public_message(),
InvokeError::new(InvokeErrorKind::UnsupportedCapability).public_message(),
InvokeError::new(InvokeErrorKind::ExecutionFailed).public_message(),
];
for i in 0..messages.len() {
for j in (i + 1)..messages.len() {
assert_ne!(messages[i], messages[j], "public messages must be distinct");
}
}
for msg in messages {
assert!(!msg.contains("detail"));
assert!(!msg.contains("guest"));
}
}
#[test]
fn invoke_error_public_message_never_contains_detail() {
let err = InvokeError::with_detail(
InvokeErrorKind::ExecutionFailed,
"SECRET-TOKEN-LEAK-ATTEMPT guest-controlled-payload",
);
assert_eq!(err.public_message(), "handler execution failed");
assert_eq!(err.stable_code(), "handlerInternalError");
assert_eq!(
err.detail(),
Some("SECRET-TOKEN-LEAK-ATTEMPT guest-controlled-payload")
);
assert!(err.to_string().contains("SECRET-TOKEN-LEAK-ATTEMPT"));
assert!(!err.public_message().contains("SECRET"));
}
#[test]
fn invoke_error_without_detail_has_no_detail() {
let err = InvokeError::new(InvokeErrorKind::Trap);
assert_eq!(err.kind(), InvokeErrorKind::Trap);
assert_eq!(err.detail(), None);
assert_eq!(err.stable_code(), "handlerTrap");
assert_eq!(err.to_string(), "handlerTrap");
}
#[test]
fn invoke_error_detail_is_truncated_to_4kib_on_char_boundary() {
let huge = "A".repeat(MAX_DETAIL_BYTES * 4);
let err = InvokeError::with_detail(InvokeErrorKind::ExecutionFailed, huge);
let detail = err.detail().expect("detail must be stored");
assert!(
detail.len() <= MAX_DETAIL_BYTES,
"detail length {} must not exceed {}",
detail.len(),
MAX_DETAIL_BYTES
);
assert!(detail.chars().all(|c| c == 'A'));
}
#[test]
fn invoke_error_detail_truncation_respects_multibyte_chars() {
let emoji = "🌟".repeat(MAX_DETAIL_BYTES); let err = InvokeError::with_detail(InvokeErrorKind::ExecutionFailed, emoji);
let detail = err.detail().expect("detail must be stored");
assert!(detail.len() <= MAX_DETAIL_BYTES);
for c in detail.chars() {
assert_eq!(c, '🌟');
}
}
#[derive(Debug)]
struct FailingHandler {
err: InvokeError,
}
impl InvokeHandler for FailingHandler {
fn invoke(&self, _capability: &str, _input: &Value) -> Result<Value, InvokeError> {
Err(self.err.clone())
}
}
#[test]
fn engine_invoke_error_does_not_leak_guest_detail_in_message() {
let err = InvokeError::with_detail(
InvokeErrorKind::ExecutionFailed,
"leak-attempt:SECRET-TOKEN",
);
let pack = LoadedModelPack {
manifest: ModelPackManifest {
format_version: MODEL_PACK_FORMAT_VERSION,
id: "rillml.example.default".into(),
version: "0.7.0".into(),
runtime_api_version: RUNTIME_API_VERSION,
min_runtime_version: "0.7.0".into(),
publisher_key_id: "test".into(),
capabilities: vec!["rillml.example".into()],
},
model: serde_json::json!({}),
};
let sink = Arc::new(CapturingLogSink::new());
let engine = RuntimeEngine::new(pack)
.with_invoke_handler(Arc::new(FailingHandler { err }))
.with_log_sink(sink.clone());
let response = engine.handle(RuntimeRequest::Invoke {
request_id: "leak-test".into(),
api_version: RUNTIME_API_VERSION,
capability: "rillml.example".into(),
input: serde_json::json!({}),
});
match response {
EngineResponse::Error {
code,
message,
retryable,
..
} => {
assert_eq!(code, "handlerInternalError");
assert_eq!(message, "handler execution failed");
assert!(!retryable);
assert!(!message.contains("SECRET"));
assert!(!message.contains("leak-attempt"));
}
_ => panic!("expected EngineResponse::Error"),
}
let messages = sink.messages();
assert_eq!(
messages.len(),
1,
"the engine must log the invoke error exactly once"
);
assert!(messages[0].contains("SECRET-TOKEN"));
}
#[test]
fn engine_log_does_not_emit_oversized_guest_detail() {
let huge_detail = "X".repeat(MAX_DETAIL_BYTES * 4); let err = InvokeError::with_detail(InvokeErrorKind::ExecutionFailed, huge_detail);
let pack = LoadedModelPack {
manifest: ModelPackManifest {
format_version: MODEL_PACK_FORMAT_VERSION,
id: "rillml.example.default".into(),
version: "0.7.0".into(),
runtime_api_version: RUNTIME_API_VERSION,
min_runtime_version: "0.7.0".into(),
publisher_key_id: "test".into(),
capabilities: vec!["rillml.example".into()],
},
model: serde_json::json!({}),
};
let sink = Arc::new(CapturingLogSink::new());
let engine = RuntimeEngine::new(pack)
.with_invoke_handler(Arc::new(FailingHandler { err }))
.with_log_sink(sink.clone());
let _ = engine.handle(RuntimeRequest::Invoke {
request_id: "oversized".into(),
api_version: RUNTIME_API_VERSION,
capability: "rillml.example".into(),
input: serde_json::json!({}),
});
let messages = sink.messages();
assert_eq!(messages.len(), 1, "exactly one log line expected");
let log_line = &messages[0];
assert!(
log_line.len() < MAX_DETAIL_BYTES * 2,
"log line length {} must be well under 2x MAX_DETAIL_BYTES ({}); \
a 16 KiB guest payload must not produce a 16 KiB log",
log_line.len(),
MAX_DETAIL_BYTES * 2
);
assert!(
log_line.len() < MAX_DETAIL_BYTES + 256,
"log line length {} must be < MAX_DETAIL_BYTES + prefix overhead",
log_line.len()
);
}
#[test]
fn engine_logs_invoke_error_exactly_once() {
let err = InvokeError::with_detail(
InvokeErrorKind::UnsupportedCapability,
"capability foo not supported",
);
let pack = LoadedModelPack {
manifest: ModelPackManifest {
format_version: MODEL_PACK_FORMAT_VERSION,
id: "rillml.example.default".into(),
version: "0.7.0".into(),
runtime_api_version: RUNTIME_API_VERSION,
min_runtime_version: "0.7.0".into(),
publisher_key_id: "test".into(),
capabilities: vec!["rillml.example".into()],
},
model: serde_json::json!({}),
};
let sink = Arc::new(CapturingLogSink::new());
let engine = RuntimeEngine::new(pack)
.with_invoke_handler(Arc::new(FailingHandler { err }))
.with_log_sink(sink.clone());
let _ = engine.handle(RuntimeRequest::Invoke {
request_id: "once".into(),
api_version: RUNTIME_API_VERSION,
capability: "rillml.example".into(),
input: serde_json::json!({}),
});
assert_eq!(
sink.messages().len(),
1,
"the engine must log the invoke error exactly once, not twice"
);
}
#[test]
fn engine_log_traps_backtrace_is_truncated() {
let fake_backtrace = "trap: unreachable\n".repeat(1024); let err = InvokeError::with_detail(InvokeErrorKind::Trap, fake_backtrace);
let pack = LoadedModelPack {
manifest: ModelPackManifest {
format_version: MODEL_PACK_FORMAT_VERSION,
id: "rillml.example.default".into(),
version: "0.7.0".into(),
runtime_api_version: RUNTIME_API_VERSION,
min_runtime_version: "0.7.0".into(),
publisher_key_id: "test".into(),
capabilities: vec!["rillml.example".into()],
},
model: serde_json::json!({}),
};
let sink = Arc::new(CapturingLogSink::new());
let engine = RuntimeEngine::new(pack)
.with_invoke_handler(Arc::new(FailingHandler { err }))
.with_log_sink(sink.clone());
let _ = engine.handle(RuntimeRequest::Invoke {
request_id: "trap-trunc".into(),
api_version: RUNTIME_API_VERSION,
capability: "rillml.example".into(),
input: serde_json::json!({}),
});
let messages = sink.messages();
assert_eq!(messages.len(), 1);
let log_line = &messages[0];
assert!(
log_line.len() < MAX_DETAIL_BYTES + 256,
"trap backtrace log must be truncated; got {} bytes",
log_line.len()
);
}
#[test]
fn engine_preserves_guest_variant_kind_for_all_wit_variants() {
for (kind, expected_message) in [
(
InvokeErrorKind::InvalidModel,
"handler rejected the model configuration",
),
(InvokeErrorKind::InvalidInput, "handler rejected the input"),
(
InvokeErrorKind::UnsupportedCapability,
"handler does not support the capability",
),
(InvokeErrorKind::ExecutionFailed, "handler execution failed"),
] {
let err = InvokeError::with_detail(kind, "guest detail");
let pack = LoadedModelPack {
manifest: ModelPackManifest {
format_version: MODEL_PACK_FORMAT_VERSION,
id: "rillml.example.default".into(),
version: "0.7.0".into(),
runtime_api_version: RUNTIME_API_VERSION,
min_runtime_version: "0.7.0".into(),
publisher_key_id: "test".into(),
capabilities: vec!["rillml.example".into()],
},
model: serde_json::json!({}),
};
let engine =
RuntimeEngine::new(pack).with_invoke_handler(Arc::new(FailingHandler { err }));
let response = engine.handle(RuntimeRequest::Invoke {
request_id: "variant".into(),
api_version: RUNTIME_API_VERSION,
capability: "rillml.example".into(),
input: serde_json::json!({}),
});
match response {
EngineResponse::Error { code, message, .. } => {
assert_eq!(
code, "handlerInternalError",
"{kind:?}: stable code must stay handlerInternalError"
);
assert_eq!(
message, expected_message,
"{kind:?}: public message mismatch"
);
}
_ => panic!("{kind:?}: expected EngineResponse::Error"),
}
}
}
}