1use std::{
4 any::Any,
5 collections::BTreeMap,
6 fs,
7 path::{Path, PathBuf},
8 rc::Rc,
9};
10
11use lenso_app_plan::{
12 CapabilityCardinality, ExecutionClassId, PluginInstancePlan, ResolvedAppPlan,
13};
14use lenso_kernel::{
15 InvocationContext, NativeRequestEndpoint, NativeStream, NativeStreamEndpoint, NativeStreamItem,
16 NativeStreamSession, PluginDependencies, PluginDependencyHandle, PluginStreamDependencyHandle,
17 PreparedBinding, PreparedNativeApp, PreparedNativePlugin, PreparedStreamBinding,
18 RuntimeFailure, StreamCapability, StreamEvent,
19};
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use sha2::{Digest, Sha256};
23
24#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct ArtifactHandle {
27 path: PathBuf,
28 digest: String,
29 size: u64,
30}
31
32impl ArtifactHandle {
33 pub fn open(
35 path: impl Into<PathBuf>,
36 expected_digest: &str,
37 expected_size: u64,
38 ) -> Result<Self, RuntimeFailure> {
39 validate_digest(expected_digest)?;
40 let path = path.into();
41 let metadata =
42 fs::symlink_metadata(&path).map_err(|error| invalid_artifact(&path, error))?;
43 if !metadata.file_type().is_file() || metadata.file_type().is_symlink() {
44 return Err(RuntimeFailure::InvalidResolvedPlan {
45 detail: format!("Artifact `{}` is not a regular file", path.display()),
46 });
47 }
48 if metadata.len() != expected_size {
49 return Err(RuntimeFailure::InvalidResolvedPlan {
50 detail: format!(
51 "Artifact `{}` size mismatch: expected {expected_size}, got {}",
52 path.display(),
53 metadata.len()
54 ),
55 });
56 }
57 let bytes = fs::read(&path).map_err(|error| invalid_artifact(&path, error))?;
58 let actual_digest = format!("sha256:{}", hex::encode(Sha256::digest(&bytes)));
59 if actual_digest != expected_digest {
60 return Err(RuntimeFailure::InvalidResolvedPlan {
61 detail: format!("Artifact `{}` digest mismatch", path.display()),
62 });
63 }
64 Ok(Self {
65 path,
66 digest: actual_digest,
67 size: metadata.len(),
68 })
69 }
70
71 pub fn path(&self) -> &Path {
73 &self.path
74 }
75
76 pub fn digest(&self) -> &str {
78 &self.digest
79 }
80
81 pub const fn size(&self) -> u64 {
83 self.size
84 }
85
86 pub fn read_verified(&self) -> Result<Vec<u8>, RuntimeFailure> {
88 let verified = Self::open(&self.path, &self.digest, self.size)?;
89 fs::read(verified.path).map_err(|error| invalid_artifact(&self.path, error))
90 }
91}
92
93#[derive(Clone, Debug, Default)]
95pub struct ArtifactCatalog(BTreeMap<String, ArtifactHandle>);
96
97impl ArtifactCatalog {
98 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn with_artifact(
105 mut self,
106 instance_key: impl Into<String>,
107 artifact: ArtifactHandle,
108 ) -> Result<Self, RuntimeFailure> {
109 let instance_key = instance_key.into();
110 if self.0.insert(instance_key.clone(), artifact).is_some() {
111 return Err(RuntimeFailure::InvalidResolvedPlan {
112 detail: format!("duplicate Artifact authority for Instance `{instance_key}`"),
113 });
114 }
115 Ok(self)
116 }
117
118 pub fn require(&self, instance_key: &str) -> Result<&ArtifactHandle, RuntimeFailure> {
120 self.0
121 .get(instance_key)
122 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
123 detail: format!("no admitted Artifact for Instance `{instance_key}`"),
124 })
125 }
126}
127
128pub trait JsonCapabilityCodec: std::fmt::Debug + 'static {
130 fn capability_id(&self) -> &'static str;
132 fn descriptor_version(&self) -> &'static str;
134 fn request_operations(&self) -> &'static [&'static str];
136 fn stream_operations(&self) -> &'static [&'static str] {
138 &[]
139 }
140 fn encode_request(&self, operation: &str, request: &dyn Any) -> Result<Value, RuntimeFailure>;
142 fn decode_response(
144 &self,
145 operation: &str,
146 value: Value,
147 ) -> Result<Box<dyn Any>, RuntimeFailure>;
148 fn decode_domain_error(
150 &self,
151 operation: &str,
152 value: Value,
153 ) -> Result<Box<dyn Any>, RuntimeFailure>;
154 fn encode_stream_open(
156 &self,
157 operation: &str,
158 request: &dyn Any,
159 ) -> Result<Value, RuntimeFailure> {
160 let _ = request;
161 Err(unknown_operation(self.capability_id(), operation))
162 }
163 fn encode_stream_message(
165 &self,
166 operation: &str,
167 message: &dyn Any,
168 ) -> Result<Value, RuntimeFailure> {
169 let _ = message;
170 Err(unknown_operation(self.capability_id(), operation))
171 }
172 fn decode_stream_message(
174 &self,
175 operation: &str,
176 value: Value,
177 ) -> Result<Box<dyn Any>, RuntimeFailure> {
178 let _ = value;
179 Err(unknown_operation(self.capability_id(), operation))
180 }
181 fn decode_stream_domain_error(
183 &self,
184 operation: &str,
185 value: Value,
186 ) -> Result<Box<dyn Any>, RuntimeFailure> {
187 let _ = value;
188 Err(unknown_operation(self.capability_id(), operation))
189 }
190 fn invoke_host_request(
192 &self,
193 dependency: PluginDependencyHandle,
194 operation: String,
195 request: Value,
196 context: InvocationContext,
197 ) -> JsonHostRequestFuture {
198 let _ = (dependency, request, context);
199 Box::pin(futures::future::ready(Err(unknown_operation(
200 self.capability_id(),
201 &operation,
202 ))))
203 }
204 fn open_host_stream(
206 &self,
207 dependency: PluginStreamDependencyHandle,
208 operation: String,
209 request: Value,
210 context: InvocationContext,
211 ) -> JsonHostStreamOpenFuture {
212 let _ = (dependency, request, context);
213 Box::pin(futures::future::ready(Err(unknown_operation(
214 self.capability_id(),
215 &operation,
216 ))))
217 }
218}
219
220#[derive(Debug)]
222pub enum JsonInvocationOutcome {
223 Success(Value),
225 DomainError(Value),
227}
228
229pub fn json_runtime_failure(error: &RuntimeFailure) -> Value {
231 match error {
232 RuntimeFailure::Unavailable { capability } => serde_json::json!({
233 "kind": "unavailable",
234 "capability": capability,
235 }),
236 RuntimeFailure::UnknownOperation {
237 capability,
238 operation,
239 } => serde_json::json!({
240 "kind": "unknown_operation",
241 "capability": capability,
242 "operation": operation,
243 }),
244 RuntimeFailure::AmbiguousBinding {
245 capability,
246 providers,
247 } => serde_json::json!({
248 "kind": "ambiguous_binding",
249 "capability": capability,
250 "providers": providers,
251 }),
252 RuntimeFailure::ProtocolViolation { capability } => serde_json::json!({
253 "kind": "protocol_violation",
254 "capability": capability,
255 }),
256 RuntimeFailure::AdmissionClosed => serde_json::json!({ "kind": "admission_closed" }),
257 RuntimeFailure::ResourceExhausted {
258 capability,
259 operation,
260 } => serde_json::json!({
261 "kind": "resource_exhausted",
262 "capability": capability,
263 "operation": operation,
264 }),
265 RuntimeFailure::DeadlineExceeded { request_id } => serde_json::json!({
266 "kind": "deadline_exceeded",
267 "request_id": request_id.to_string(),
268 }),
269 RuntimeFailure::Cancelled { request_id } => serde_json::json!({
270 "kind": "cancelled",
271 "request_id": request_id.to_string(),
272 }),
273 RuntimeFailure::MissingPluginFactory { .. }
274 | RuntimeFailure::UnavailableExecutionClass { .. }
275 | RuntimeFailure::InvalidResolvedPlan { .. }
276 | RuntimeFailure::Internal { .. }
277 | RuntimeFailure::PluginFailure { .. }
278 | RuntimeFailure::PluginRestartExhausted { .. } => {
279 serde_json::json!({ "kind": "internal" })
280 }
281 }
282}
283
284pub fn json_host_invocation_envelope(
286 outcome: Result<JsonInvocationOutcome, RuntimeFailure>,
287) -> Value {
288 match outcome {
289 Ok(JsonInvocationOutcome::Success(value)) => serde_json::json!({ "ok": value }),
290 Ok(JsonInvocationOutcome::DomainError(value)) => serde_json::json!({ "error": value }),
291 Err(error) => serde_json::json!({ "runtime": json_runtime_failure(&error) }),
292 }
293}
294
295pub type JsonHostRequestFuture =
297 futures::future::LocalBoxFuture<'static, Result<JsonInvocationOutcome, RuntimeFailure>>;
298
299pub trait JsonHostStreamSession: std::fmt::Debug + 'static {
301 fn send(
302 self: Rc<Self>,
303 message: Value,
304 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
305 fn receive(
306 self: Rc<Self>,
307 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>>;
308 fn close_send(
309 self: Rc<Self>,
310 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
311 fn cancel(&self);
312}
313
314pub type JsonHostStreamOpenFuture = futures::future::LocalBoxFuture<
316 'static,
317 Result<Result<Rc<dyn JsonHostStreamSession>, Value>, RuntimeFailure>,
318>;
319
320type DecodeStreamMessage<C> =
321 Rc<dyn Fn(Value) -> Result<<C as StreamCapability>::Message, RuntimeFailure>>;
322type EncodeStreamMessage<C> =
323 Rc<dyn Fn(<C as StreamCapability>::Message) -> Result<Value, RuntimeFailure>>;
324type EncodeStreamError<C> =
325 Rc<dyn Fn(<C as StreamCapability>::DomainError) -> Result<Value, RuntimeFailure>>;
326
327pub fn json_host_stream<C: StreamCapability>(
329 stream: NativeStream<C>,
330 decode_message: impl Fn(Value) -> Result<C::Message, RuntimeFailure> + 'static,
331 encode_message: impl Fn(C::Message) -> Result<Value, RuntimeFailure> + 'static,
332 encode_error: impl Fn(C::DomainError) -> Result<Value, RuntimeFailure> + 'static,
333) -> Rc<dyn JsonHostStreamSession> {
334 Rc::new(TypedJsonHostStream {
335 stream: Rc::new(stream),
336 decode_message: Rc::new(decode_message),
337 encode_message: Rc::new(encode_message),
338 encode_error: Rc::new(encode_error),
339 })
340}
341
342struct TypedJsonHostStream<C: StreamCapability> {
343 stream: Rc<NativeStream<C>>,
344 decode_message: DecodeStreamMessage<C>,
345 encode_message: EncodeStreamMessage<C>,
346 encode_error: EncodeStreamError<C>,
347}
348
349impl<C: StreamCapability> std::fmt::Debug for TypedJsonHostStream<C> {
350 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 formatter
352 .debug_struct("TypedJsonHostStream")
353 .field("capability", &C::ID)
354 .finish_non_exhaustive()
355 }
356}
357
358impl<C: StreamCapability> JsonHostStreamSession for TypedJsonHostStream<C> {
359 fn send(
360 self: Rc<Self>,
361 message: Value,
362 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
363 Box::pin(async move {
364 let message = (self.decode_message)(message)?;
365 self.stream.send(message).await
366 })
367 }
368
369 fn receive(
370 self: Rc<Self>,
371 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>> {
372 Box::pin(async move {
373 match self.stream.receive().await? {
374 StreamEvent::Message(message) => {
375 (self.encode_message)(message).map(JsonStreamItem::Message)
376 }
377 StreamEvent::PeerHalfClosed => Ok(JsonStreamItem::PeerHalfClosed),
378 StreamEvent::Terminal(Ok(())) => Ok(JsonStreamItem::Terminal(Ok(()))),
379 StreamEvent::Terminal(Err(error)) => {
380 (self.encode_error)(error).map(|error| JsonStreamItem::Terminal(Err(error)))
381 }
382 }
383 })
384 }
385
386 fn close_send(
387 self: Rc<Self>,
388 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
389 Box::pin(async move { self.stream.close_send().await })
390 }
391
392 fn cancel(&self) {
393 self.stream.cancel();
394 }
395}
396
397pub const JSON_REQUEST_ABI_V1: &str = "lenso.json-request@1";
399
400pub const JSON_INTERACTIONS_ABI_V1: &str = "lenso.json-interactions@1";
402
403pub const JSON_HOST_IMPORTS_ABI_V1: &str = "lenso.json-host-imports@1";
405
406#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
408#[serde(deny_unknown_fields)]
409pub struct JsonPluginDescriptor {
410 pub abi: String,
411 pub capabilities: Vec<JsonCapabilityDescriptor>,
412 #[serde(default, skip_serializing_if = "Vec::is_empty")]
413 pub required_capabilities: Vec<JsonRequiredCapabilityDescriptor>,
414}
415
416#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
418#[serde(deny_unknown_fields)]
419pub struct JsonCapabilityDescriptor {
420 pub capability_id: String,
421 pub descriptor_version: String,
422 pub request_operations: Vec<String>,
423 #[serde(default, skip_serializing_if = "Vec::is_empty")]
424 pub stream_operations: Vec<String>,
425}
426
427#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
429#[serde(deny_unknown_fields)]
430pub struct JsonRequiredCapabilityDescriptor {
431 pub capability_id: String,
432 pub descriptor_version: String,
433 pub cardinality: CapabilityCardinality,
434}
435
436pub fn expected_json_plugin_descriptor(
438 instance: &PluginInstancePlan,
439) -> Result<JsonPluginDescriptor, RuntimeFailure> {
440 let mut capabilities = Vec::with_capacity(instance.provided_capabilities().len());
441 for descriptor in instance.provided_capabilities() {
442 if !descriptor.event_operations().is_empty() {
443 return Err(RuntimeFailure::InvalidResolvedPlan {
444 detail: format!(
445 "Execution class `{}` does not support Event endpoints",
446 instance.execution_class()
447 ),
448 });
449 }
450 capabilities.push(JsonCapabilityDescriptor {
451 capability_id: descriptor.capability_id().to_owned(),
452 descriptor_version: descriptor.descriptor_version().to_owned(),
453 request_operations: descriptor
454 .request_operations()
455 .into_iter()
456 .map(str::to_owned)
457 .collect(),
458 stream_operations: descriptor
459 .stream_operations()
460 .into_iter()
461 .map(str::to_owned)
462 .collect(),
463 });
464 }
465 capabilities.sort();
466 if capabilities
467 .windows(2)
468 .any(|pair| pair[0].capability_id == pair[1].capability_id)
469 {
470 return Err(RuntimeFailure::InvalidResolvedPlan {
471 detail: format!(
472 "Instance `{}` declares a duplicate Capability",
473 instance.instance_key()
474 ),
475 });
476 }
477 let mut required_capabilities = instance
478 .required_capabilities()
479 .iter()
480 .map(|requirement| JsonRequiredCapabilityDescriptor {
481 capability_id: requirement.capability_id().to_owned(),
482 descriptor_version: requirement.descriptor_version().to_owned(),
483 cardinality: requirement.cardinality(),
484 })
485 .collect::<Vec<_>>();
486 sort_required_capabilities(&mut required_capabilities);
487 Ok(JsonPluginDescriptor {
488 abi: if !required_capabilities.is_empty() {
489 JSON_HOST_IMPORTS_ABI_V1
490 } else if capabilities
491 .iter()
492 .any(|capability| !capability.stream_operations.is_empty())
493 {
494 JSON_INTERACTIONS_ABI_V1
495 } else {
496 JSON_REQUEST_ABI_V1
497 }
498 .to_owned(),
499 capabilities,
500 required_capabilities,
501 })
502}
503
504pub fn validate_json_plugin_descriptor(
506 instance: &PluginInstancePlan,
507 encoded: &str,
508) -> Result<(), RuntimeFailure> {
509 let mut actual = serde_json::from_str::<JsonPluginDescriptor>(encoded).map_err(|_| {
510 RuntimeFailure::ProtocolViolation {
511 capability: "lenso.json-request@1",
512 }
513 })?;
514 actual.capabilities.sort();
515 sort_required_capabilities(&mut actual.required_capabilities);
516 let expected = expected_json_plugin_descriptor(instance)?;
517 if actual != expected {
518 return Err(RuntimeFailure::InvalidResolvedPlan {
519 detail: format!(
520 "guest descriptor does not match resolved Instance `{}`",
521 instance.instance_key()
522 ),
523 });
524 }
525 Ok(())
526}
527
528fn sort_required_capabilities(requirements: &mut [JsonRequiredCapabilityDescriptor]) {
529 requirements.sort_by(|left, right| {
530 (
531 &left.capability_id,
532 &left.descriptor_version,
533 cardinality_order(left.cardinality),
534 )
535 .cmp(&(
536 &right.capability_id,
537 &right.descriptor_version,
538 cardinality_order(right.cardinality),
539 ))
540 });
541}
542
543const fn cardinality_order(cardinality: CapabilityCardinality) -> u8 {
544 match cardinality {
545 CapabilityCardinality::One => 0,
546 CapabilityCardinality::Optional => 1,
547 CapabilityCardinality::Many => 2,
548 }
549}
550
551pub trait JsonRequestTransport: std::fmt::Debug + 'static {
553 fn invoke(
554 self: Rc<Self>,
555 capability: String,
556 operation: String,
557 request_json: String,
558 context: InvocationContext,
559 ) -> futures::future::LocalBoxFuture<'static, Result<JsonInvocationOutcome, RuntimeFailure>>;
560}
561
562#[derive(Debug)]
564pub enum JsonStreamItem {
565 Message(Value),
566 PeerHalfClosed,
567 Terminal(Result<(), Value>),
568}
569
570#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
572#[serde(
573 tag = "kind",
574 content = "value",
575 rename_all = "kebab-case",
576 deny_unknown_fields
577)]
578pub enum JsonStreamFrame {
579 Message(Value),
580 PeerHalfClosed,
581 TerminalSuccess,
582 TerminalError(Value),
583}
584
585#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
587pub struct JsonHostBindingDescriptor {
588 pub binding_id: u32,
589 pub provider_instance: String,
590 pub capability_id: String,
591 pub descriptor_version: String,
592 pub request_operations: Vec<String>,
593 pub stream_operations: Vec<String>,
594}
595
596#[derive(Clone)]
597struct JsonHostBinding {
598 descriptor: JsonHostBindingDescriptor,
599 codec: Rc<dyn JsonCapabilityCodec>,
600 request: Option<PluginDependencyHandle>,
601 stream: Option<PluginStreamDependencyHandle>,
602}
603
604impl std::fmt::Debug for JsonHostBinding {
605 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606 formatter
607 .debug_struct("JsonHostBinding")
608 .field("descriptor", &self.descriptor)
609 .finish_non_exhaustive()
610 }
611}
612
613#[derive(Debug)]
615pub struct JsonHostImports {
616 codecs: BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
617 bindings: std::cell::RefCell<Option<Vec<JsonHostBinding>>>,
618 streams: std::cell::RefCell<BTreeMap<u64, Rc<dyn JsonHostStreamSession>>>,
619 next_stream_id: std::cell::Cell<u64>,
620 max_streams: usize,
621}
622
623impl JsonHostImports {
624 pub fn new(
626 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
627 max_streams: usize,
628 ) -> Result<Self, RuntimeFailure> {
629 let mut by_capability = BTreeMap::new();
630 for codec in codecs {
631 let capability = codec.capability_id().to_owned();
632 if by_capability.insert(capability.clone(), codec).is_some() {
633 return Err(RuntimeFailure::InvalidResolvedPlan {
634 detail: format!("duplicate guest import codec for Capability `{capability}`"),
635 });
636 }
637 }
638 Ok(Self {
639 codecs: by_capability,
640 bindings: std::cell::RefCell::new(None),
641 streams: std::cell::RefCell::new(BTreeMap::new()),
642 next_stream_id: std::cell::Cell::new(1),
643 max_streams,
644 })
645 }
646
647 pub fn activate(&self, dependencies: &PluginDependencies) -> Result<(), RuntimeFailure> {
649 if self.bindings.borrow().is_some() {
650 return Err(RuntimeFailure::Internal {
651 detail: "guest Capability imports were activated twice".to_owned(),
652 });
653 }
654 let mut bindings = Vec::with_capacity(dependencies.len());
655 for (index, dependency) in dependencies.bindings().iter().enumerate() {
656 let codec = self
657 .codecs
658 .get(dependency.capability_id())
659 .cloned()
660 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
661 detail: format!(
662 "no generated guest import codec for Capability `{}`",
663 dependency.capability_id()
664 ),
665 })?;
666 let request = dependency.handle();
667 let stream = dependency.stream_handle();
668 validate_host_binding(&codec, request.as_ref(), stream.as_ref())?;
669 let binding_id =
670 u32::try_from(index).map_err(|_| RuntimeFailure::InvalidResolvedPlan {
671 detail: "guest import binding table exceeds u32 identity space".to_owned(),
672 })?;
673 bindings.push(JsonHostBinding {
674 descriptor: JsonHostBindingDescriptor {
675 binding_id,
676 provider_instance: dependency.provider_instance().to_owned(),
677 capability_id: dependency.capability_id().to_owned(),
678 descriptor_version: codec.descriptor_version().to_owned(),
679 request_operations: request.as_ref().map_or_else(Vec::new, |handle| {
680 handle
681 .operations()
682 .iter()
683 .map(|item| (*item).to_owned())
684 .collect()
685 }),
686 stream_operations: stream.as_ref().map_or_else(Vec::new, |handle| {
687 handle
688 .operations()
689 .iter()
690 .map(|item| (*item).to_owned())
691 .collect()
692 }),
693 },
694 codec,
695 request,
696 stream,
697 });
698 }
699 self.bindings.replace(Some(bindings));
700 Ok(())
701 }
702
703 pub fn descriptors(&self) -> Result<Vec<JsonHostBindingDescriptor>, RuntimeFailure> {
705 self.bindings
706 .borrow()
707 .as_ref()
708 .map(|bindings| {
709 bindings
710 .iter()
711 .map(|binding| binding.descriptor.clone())
712 .collect()
713 })
714 .ok_or(RuntimeFailure::AdmissionClosed)
715 }
716
717 pub fn invoke(
719 &self,
720 binding_id: u32,
721 operation: String,
722 request: Value,
723 context: InvocationContext,
724 ) -> JsonHostRequestFuture {
725 let binding = match self.binding(binding_id) {
726 Ok(binding) => binding,
727 Err(error) => return Box::pin(futures::future::ready(Err(error))),
728 };
729 let Some(dependency) = binding.request else {
730 return Box::pin(futures::future::ready(Err(
731 RuntimeFailure::UnknownOperation {
732 capability: binding.codec.capability_id(),
733 operation,
734 },
735 )));
736 };
737 binding
738 .codec
739 .invoke_host_request(dependency, operation, request, context)
740 }
741
742 pub fn open_stream(
744 self: Rc<Self>,
745 binding_id: u32,
746 operation: String,
747 request: Value,
748 context: InvocationContext,
749 ) -> futures::future::LocalBoxFuture<'static, Result<Result<u64, Value>, RuntimeFailure>> {
750 Box::pin(async move {
751 if self.streams.borrow().len() >= self.max_streams {
752 return Err(RuntimeFailure::ResourceExhausted {
753 capability: "lenso.json-host-imports@1",
754 operation: "stream-open".to_owned(),
755 });
756 }
757 let binding = self.binding(binding_id)?;
758 let dependency = binding
759 .stream
760 .ok_or_else(|| RuntimeFailure::UnknownOperation {
761 capability: binding.codec.capability_id(),
762 operation: operation.clone(),
763 })?;
764 match binding
765 .codec
766 .open_host_stream(dependency, operation, request, context)
767 .await?
768 {
769 Ok(stream) => {
770 let stream_id = self.next_stream_id.get();
771 let next =
772 stream_id
773 .checked_add(1)
774 .ok_or(RuntimeFailure::ResourceExhausted {
775 capability: "lenso.json-host-imports@1",
776 operation: "stream-open".to_owned(),
777 })?;
778 self.next_stream_id.set(next);
779 self.streams.borrow_mut().insert(stream_id, stream);
780 Ok(Ok(stream_id))
781 }
782 Err(error) => Ok(Err(error)),
783 }
784 })
785 }
786
787 pub fn send_stream(
789 &self,
790 stream_id: u64,
791 message: Value,
792 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
793 match self.stream(stream_id) {
794 Ok(stream) => stream.send(message),
795 Err(error) => Box::pin(futures::future::ready(Err(error))),
796 }
797 }
798
799 pub fn receive_stream(
801 self: Rc<Self>,
802 stream_id: u64,
803 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>> {
804 Box::pin(async move {
805 let stream = self.stream(stream_id)?;
806 let item = stream.receive().await?;
807 if matches!(item, JsonStreamItem::Terminal(_)) {
808 self.streams.borrow_mut().remove(&stream_id);
809 }
810 Ok(item)
811 })
812 }
813
814 pub fn close_stream_send(
816 &self,
817 stream_id: u64,
818 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
819 match self.stream(stream_id) {
820 Ok(stream) => stream.close_send(),
821 Err(error) => Box::pin(futures::future::ready(Err(error))),
822 }
823 }
824
825 pub fn cancel_stream(&self, stream_id: u64) -> Result<(), RuntimeFailure> {
827 let stream = self
828 .streams
829 .borrow_mut()
830 .remove(&stream_id)
831 .ok_or_else(unknown_host_stream)?;
832 stream.cancel();
833 Ok(())
834 }
835
836 pub fn deactivate(&self) {
838 self.bindings.replace(None);
839 for (_, stream) in std::mem::take(&mut *self.streams.borrow_mut()) {
840 stream.cancel();
841 }
842 }
843
844 fn binding(&self, binding_id: u32) -> Result<JsonHostBinding, RuntimeFailure> {
845 let bindings = self.bindings.borrow();
846 let bindings = bindings.as_ref().ok_or(RuntimeFailure::AdmissionClosed)?;
847 bindings
848 .get(binding_id as usize)
849 .cloned()
850 .ok_or(RuntimeFailure::ProtocolViolation {
851 capability: JSON_HOST_IMPORTS_ABI_V1,
852 })
853 }
854
855 fn stream(&self, stream_id: u64) -> Result<Rc<dyn JsonHostStreamSession>, RuntimeFailure> {
856 self.streams
857 .borrow()
858 .get(&stream_id)
859 .cloned()
860 .ok_or_else(unknown_host_stream)
861 }
862}
863
864fn validate_host_binding(
865 codec: &Rc<dyn JsonCapabilityCodec>,
866 request: Option<&PluginDependencyHandle>,
867 stream: Option<&PluginStreamDependencyHandle>,
868) -> Result<(), RuntimeFailure> {
869 for (capability, version) in request
870 .map(|handle| (handle.capability_id(), handle.descriptor_version()))
871 .into_iter()
872 .chain(stream.map(|handle| (handle.capability_id(), handle.descriptor_version())))
873 {
874 if capability != codec.capability_id() || version != codec.descriptor_version() {
875 return Err(RuntimeFailure::ProtocolViolation {
876 capability: codec.capability_id(),
877 });
878 }
879 }
880 Ok(())
881}
882
883fn unknown_host_stream() -> RuntimeFailure {
884 RuntimeFailure::ProtocolViolation {
885 capability: "lenso.json-host-imports@1",
886 }
887}
888
889impl JsonStreamFrame {
890 pub fn decode(
892 encoded: &str,
893 capability: &'static str,
894 ) -> Result<JsonStreamItem, RuntimeFailure> {
895 match serde_json::from_str(encoded)
896 .map_err(|_| RuntimeFailure::ProtocolViolation { capability })?
897 {
898 Self::Message(value) => Ok(JsonStreamItem::Message(value)),
899 Self::PeerHalfClosed => Ok(JsonStreamItem::PeerHalfClosed),
900 Self::TerminalSuccess => Ok(JsonStreamItem::Terminal(Ok(()))),
901 Self::TerminalError(value) => Ok(JsonStreamItem::Terminal(Err(value))),
902 }
903 }
904}
905
906pub trait JsonStreamSessionTransport: std::fmt::Debug + 'static {
908 fn send(
909 self: Rc<Self>,
910 message_json: String,
911 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
912 fn receive(
913 self: Rc<Self>,
914 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>>;
915 fn close_send(
916 self: Rc<Self>,
917 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
918 fn cancel(&self);
919}
920
921pub type JsonStreamOpenFuture = futures::future::LocalBoxFuture<
923 'static,
924 Result<Result<Rc<dyn JsonStreamSessionTransport>, Value>, RuntimeFailure>,
925>;
926
927pub trait JsonStreamTransport: std::fmt::Debug + 'static {
929 fn open(
930 self: Rc<Self>,
931 capability: String,
932 operation: String,
933 request_json: String,
934 context: InvocationContext,
935 ) -> JsonStreamOpenFuture;
936}
937
938pub fn json_request_endpoints<T: JsonRequestTransport>(
940 transport: Rc<T>,
941 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
942) -> Vec<Rc<dyn NativeRequestEndpoint>> {
943 let transport: Rc<dyn JsonRequestTransport> = transport;
944 codecs
945 .into_iter()
946 .filter(|codec| !codec.request_operations().is_empty())
947 .map(|codec| {
948 Rc::new(JsonRequestEndpoint {
949 transport: transport.clone(),
950 codec,
951 }) as Rc<dyn NativeRequestEndpoint>
952 })
953 .collect()
954}
955
956pub fn json_stream_endpoints<T: JsonStreamTransport>(
958 transport: Rc<T>,
959 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
960) -> Vec<Rc<dyn NativeStreamEndpoint>> {
961 let transport: Rc<dyn JsonStreamTransport> = transport;
962 codecs
963 .into_iter()
964 .filter(|codec| !codec.stream_operations().is_empty())
965 .map(|codec| {
966 Rc::new(JsonStreamEndpoint {
967 transport: transport.clone(),
968 codec,
969 }) as Rc<dyn NativeStreamEndpoint>
970 })
971 .collect()
972}
973
974#[derive(Debug)]
975struct JsonStreamEndpoint {
976 transport: Rc<dyn JsonStreamTransport>,
977 codec: Rc<dyn JsonCapabilityCodec>,
978}
979
980impl NativeStreamEndpoint for JsonStreamEndpoint {
981 fn capability_id(&self) -> &'static str {
982 self.codec.capability_id()
983 }
984 fn descriptor_version(&self) -> &'static str {
985 self.codec.descriptor_version()
986 }
987 fn operations(&self) -> &'static [&'static str] {
988 self.codec.stream_operations()
989 }
990
991 fn open(
992 &self,
993 operation: &str,
994 request: Box<dyn Any>,
995 context: InvocationContext,
996 ) -> futures::future::LocalBoxFuture<
997 'static,
998 Result<Result<Box<dyn NativeStreamSession>, Box<dyn Any>>, RuntimeFailure>,
999 > {
1000 let transport = self.transport.clone();
1001 let codec = self.codec.clone();
1002 let operation = operation.to_owned();
1003 Box::pin(async move {
1004 if !codec.stream_operations().contains(&operation.as_str()) {
1005 return Err(unknown_operation(codec.capability_id(), &operation));
1006 }
1007 let request = codec.encode_stream_open(&operation, request.as_ref())?;
1008 let request_json =
1009 serde_json::to_string(&request).map_err(|_| RuntimeFailure::ProtocolViolation {
1010 capability: codec.capability_id(),
1011 })?;
1012 match transport
1013 .open(
1014 codec.capability_id().to_owned(),
1015 operation.clone(),
1016 request_json,
1017 context,
1018 )
1019 .await?
1020 {
1021 Ok(session) => Ok(Ok(Box::new(JsonStreamSession {
1022 session,
1023 codec,
1024 operation,
1025 }) as Box<dyn NativeStreamSession>)),
1026 Err(error) => codec.decode_stream_domain_error(&operation, error).map(Err),
1027 }
1028 })
1029 }
1030}
1031
1032#[derive(Debug)]
1033struct JsonStreamSession {
1034 session: Rc<dyn JsonStreamSessionTransport>,
1035 codec: Rc<dyn JsonCapabilityCodec>,
1036 operation: String,
1037}
1038
1039impl NativeStreamSession for JsonStreamSession {
1040 fn send(
1041 &self,
1042 message: Box<dyn Any>,
1043 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1044 let encoded = self
1045 .codec
1046 .encode_stream_message(&self.operation, message.as_ref())
1047 .and_then(|value| {
1048 serde_json::to_string(&value).map_err(|_| RuntimeFailure::ProtocolViolation {
1049 capability: self.codec.capability_id(),
1050 })
1051 });
1052 let session = self.session.clone();
1053 Box::pin(async move { session.send(encoded?).await })
1054 }
1055
1056 fn receive(
1057 &self,
1058 ) -> futures::future::LocalBoxFuture<'static, Result<NativeStreamItem, RuntimeFailure>> {
1059 let session = self.session.clone();
1060 let codec = self.codec.clone();
1061 let operation = self.operation.clone();
1062 Box::pin(async move {
1063 match session.receive().await? {
1064 JsonStreamItem::Message(value) => codec
1065 .decode_stream_message(&operation, value)
1066 .map(NativeStreamItem::Message),
1067 JsonStreamItem::PeerHalfClosed => Ok(NativeStreamItem::PeerHalfClosed),
1068 JsonStreamItem::Terminal(Ok(())) => Ok(NativeStreamItem::Terminal(Ok(()))),
1069 JsonStreamItem::Terminal(Err(value)) => codec
1070 .decode_stream_domain_error(&operation, value)
1071 .map(|error| NativeStreamItem::Terminal(Err(error))),
1072 }
1073 })
1074 }
1075
1076 fn close_send(&self) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1077 self.session.clone().close_send()
1078 }
1079
1080 fn cancel(&self) {
1081 self.session.cancel();
1082 }
1083}
1084
1085#[derive(Debug)]
1086struct JsonRequestEndpoint {
1087 transport: Rc<dyn JsonRequestTransport>,
1088 codec: Rc<dyn JsonCapabilityCodec>,
1089}
1090
1091impl NativeRequestEndpoint for JsonRequestEndpoint {
1092 fn capability_id(&self) -> &'static str {
1093 self.codec.capability_id()
1094 }
1095
1096 fn descriptor_version(&self) -> &'static str {
1097 self.codec.descriptor_version()
1098 }
1099
1100 fn operations(&self) -> &'static [&'static str] {
1101 self.codec.request_operations()
1102 }
1103
1104 fn invoke(
1105 &self,
1106 operation: &str,
1107 request: Box<dyn Any>,
1108 context: InvocationContext,
1109 ) -> futures::future::LocalBoxFuture<
1110 'static,
1111 Result<Result<Box<dyn Any>, Box<dyn Any>>, RuntimeFailure>,
1112 > {
1113 let transport = self.transport.clone();
1114 let codec = self.codec.clone();
1115 let operation = operation.to_owned();
1116 Box::pin(async move {
1117 if !codec.request_operations().contains(&operation.as_str()) {
1118 return Err(RuntimeFailure::UnknownOperation {
1119 capability: codec.capability_id(),
1120 operation,
1121 });
1122 }
1123 let request = codec.encode_request(&operation, request.as_ref())?;
1124 let request =
1125 serde_json::to_string(&request).map_err(|_| RuntimeFailure::ProtocolViolation {
1126 capability: codec.capability_id(),
1127 })?;
1128 match transport
1129 .invoke(
1130 codec.capability_id().to_owned(),
1131 operation.clone(),
1132 request,
1133 context,
1134 )
1135 .await?
1136 {
1137 JsonInvocationOutcome::Success(value) => {
1138 codec.decode_response(&operation, value).map(Ok)
1139 }
1140 JsonInvocationOutcome::DomainError(value) => {
1141 codec.decode_domain_error(&operation, value).map(Err)
1142 }
1143 }
1144 })
1145 }
1146}
1147
1148pub fn codecs_for_instance(
1150 instance: &PluginInstancePlan,
1151 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1152) -> Result<Vec<Rc<dyn JsonCapabilityCodec>>, RuntimeFailure> {
1153 let mut selected = Vec::with_capacity(instance.provided_capabilities().len());
1154 for descriptor in instance.provided_capabilities() {
1155 if !descriptor.event_operations().is_empty() {
1156 return Err(RuntimeFailure::InvalidResolvedPlan {
1157 detail: format!(
1158 "Execution class `{}` does not support Event endpoints",
1159 instance.execution_class()
1160 ),
1161 });
1162 }
1163 let codec = codecs.get(descriptor.capability_id()).ok_or_else(|| {
1164 RuntimeFailure::InvalidResolvedPlan {
1165 detail: format!(
1166 "no generated codec for Capability `{}`",
1167 descriptor.capability_id()
1168 ),
1169 }
1170 })?;
1171 let request_operations: Vec<_> = codec
1172 .request_operations()
1173 .iter()
1174 .map(|operation| (*operation).to_owned())
1175 .collect();
1176 let stream_operations: Vec<_> = codec
1177 .stream_operations()
1178 .iter()
1179 .map(|operation| (*operation).to_owned())
1180 .collect();
1181 let expected_request: Vec<_> = descriptor
1182 .request_operations()
1183 .into_iter()
1184 .map(str::to_owned)
1185 .collect();
1186 let expected_stream: Vec<_> = descriptor
1187 .stream_operations()
1188 .into_iter()
1189 .map(str::to_owned)
1190 .collect();
1191 if codec.descriptor_version() != descriptor.descriptor_version()
1192 || request_operations != expected_request
1193 || stream_operations != expected_stream
1194 {
1195 return Err(RuntimeFailure::ProtocolViolation {
1196 capability: codec.capability_id(),
1197 });
1198 }
1199 selected.push(codec.clone());
1200 }
1201 Ok(selected)
1202}
1203
1204pub fn codecs_for_requirements(
1206 instance: &PluginInstancePlan,
1207 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1208) -> Result<Vec<Rc<dyn JsonCapabilityCodec>>, RuntimeFailure> {
1209 let mut selected = Vec::with_capacity(instance.required_capabilities().len());
1210 for requirement in instance.required_capabilities() {
1211 let codec = codecs.get(requirement.capability_id()).ok_or_else(|| {
1212 RuntimeFailure::InvalidResolvedPlan {
1213 detail: format!(
1214 "no generated guest import codec for Capability `{}`",
1215 requirement.capability_id()
1216 ),
1217 }
1218 })?;
1219 if codec.descriptor_version() != requirement.descriptor_version() {
1220 return Err(RuntimeFailure::ProtocolViolation {
1221 capability: codec.capability_id(),
1222 });
1223 }
1224 selected.push(codec.clone());
1225 }
1226 Ok(selected)
1227}
1228
1229pub fn prepare_request_app(
1231 plan: &ResolvedAppPlan,
1232 execution_class: &ExecutionClassId,
1233 generations: BTreeMap<String, PreparedNativePlugin>,
1234) -> Result<PreparedNativeApp, RuntimeFailure> {
1235 let selected_instances = plan
1236 .plugin_instances()
1237 .iter()
1238 .filter(|instance| instance.execution_class() == execution_class)
1239 .map(|instance| instance.instance_key().to_owned())
1240 .collect::<std::collections::BTreeSet<_>>();
1241 let mut endpoints = BTreeMap::new();
1242 let mut stream_endpoints = BTreeMap::new();
1243 for (instance_key, generation) in &generations {
1244 for endpoint in generation.endpoints() {
1245 let identity = (instance_key.clone(), endpoint.capability_id().to_owned());
1246 if endpoints.insert(identity, endpoint.clone()).is_some() {
1247 return Err(RuntimeFailure::InvalidResolvedPlan {
1248 detail: format!("duplicate request endpoint on Instance `{instance_key}`"),
1249 });
1250 }
1251 }
1252 for endpoint in generation.stream_endpoints() {
1253 let identity = (instance_key.clone(), endpoint.capability_id().to_owned());
1254 if stream_endpoints
1255 .insert(identity, endpoint.clone())
1256 .is_some()
1257 {
1258 return Err(RuntimeFailure::InvalidResolvedPlan {
1259 detail: format!("duplicate stream endpoint on Instance `{instance_key}`"),
1260 });
1261 }
1262 }
1263 }
1264 for instance in plan
1265 .plugin_instances()
1266 .iter()
1267 .filter(|instance| selected_instances.contains(instance.instance_key()))
1268 {
1269 if !generations.contains_key(instance.instance_key()) {
1270 return Err(RuntimeFailure::InvalidResolvedPlan {
1271 detail: format!("Adapter omitted Instance `{}`", instance.instance_key()),
1272 });
1273 }
1274 }
1275 let mut bindings = Vec::new();
1276 let mut stream_bindings = Vec::new();
1277 for binding in plan.capability_bindings() {
1278 let key = (
1279 binding.provider_instance().to_owned(),
1280 binding.capability_id().to_owned(),
1281 );
1282 let request_endpoint = endpoints.get(&key);
1283 let stream_endpoint = stream_endpoints.get(&key);
1284 if let Some(endpoint) = request_endpoint {
1285 bindings.push(PreparedBinding::new(
1286 binding.consumer_instance(),
1287 binding.provider_instance(),
1288 endpoint.clone(),
1289 ));
1290 }
1291 if let Some(endpoint) = stream_endpoint {
1292 stream_bindings.push(PreparedStreamBinding::new(
1293 binding.consumer_instance(),
1294 binding.provider_instance(),
1295 endpoint.clone(),
1296 ));
1297 }
1298 if request_endpoint.is_none()
1299 && stream_endpoint.is_none()
1300 && selected_instances.contains(binding.provider_instance())
1301 {
1302 return Err(RuntimeFailure::InvalidResolvedPlan {
1303 detail: format!(
1304 "Adapter omitted Capability `{}` endpoint for Instance `{}`",
1305 binding.capability_id(),
1306 binding.provider_instance()
1307 ),
1308 });
1309 }
1310 }
1311 Ok(PreparedNativeApp::new(bindings, generations).with_stream_bindings(stream_bindings))
1312}
1313
1314pub fn require_operation(
1316 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1317 capability_id: &str,
1318 operation: &str,
1319) -> Result<Rc<dyn JsonCapabilityCodec>, RuntimeFailure> {
1320 let codec =
1321 codecs
1322 .get(capability_id)
1323 .cloned()
1324 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
1325 detail: format!("no generated codec for Capability `{capability_id}`"),
1326 })?;
1327 if !codec.request_operations().contains(&operation) {
1328 return Err(RuntimeFailure::UnknownOperation {
1329 capability: codec.capability_id(),
1330 operation: operation.to_owned(),
1331 });
1332 }
1333 Ok(codec)
1334}
1335
1336fn unknown_operation(capability: &'static str, operation: &str) -> RuntimeFailure {
1337 RuntimeFailure::UnknownOperation {
1338 capability,
1339 operation: operation.to_owned(),
1340 }
1341}
1342
1343fn validate_digest(digest: &str) -> Result<(), RuntimeFailure> {
1344 let valid = digest.strip_prefix("sha256:").is_some_and(|hex| {
1345 hex.len() == 64
1346 && hex
1347 .bytes()
1348 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
1349 });
1350 if valid {
1351 Ok(())
1352 } else {
1353 Err(RuntimeFailure::InvalidResolvedPlan {
1354 detail: format!("invalid canonical SHA-256 digest `{digest}`"),
1355 })
1356 }
1357}
1358
1359fn invalid_artifact(path: &Path, error: impl std::fmt::Display) -> RuntimeFailure {
1360 RuntimeFailure::InvalidResolvedPlan {
1361 detail: format!("cannot read Artifact `{}`: {error}", path.display()),
1362 }
1363}
1364
1365#[cfg(test)]
1366mod tests {
1367 use std::io::Write;
1368
1369 use super::*;
1370
1371 #[test]
1372 fn artifact_handle_rejects_digest_drift() {
1373 let mut file = tempfile::NamedTempFile::new().unwrap();
1374 file.write_all(b"first").unwrap();
1375 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"first")));
1376 let handle = ArtifactHandle::open(file.path(), &digest, 5).unwrap();
1377 file.as_file_mut().set_len(0).unwrap();
1378 file.write_all(b"other").unwrap();
1379 assert!(handle.read_verified().is_err());
1380 }
1381}