1use std::{
4 any::Any,
5 collections::BTreeMap,
6 env, fs,
7 io::Write as _,
8 path::{Path, PathBuf},
9 rc::Rc,
10 sync::Arc,
11};
12
13use lenso_app_plan::{
14 CapabilityCardinality, ExecutionClassId, PluginInstancePlan, ResolvedAppPlan,
15};
16use lenso_kernel::{
17 InvocationContext, NativeRequestEndpoint, NativeStream, NativeStreamEndpoint, NativeStreamItem,
18 NativeStreamSession, PluginDependencies, PluginDependencyHandle, PluginStreamDependencyHandle,
19 PreparedBinding, PreparedNativeApp, PreparedNativePlugin, PreparedStreamBinding,
20 RuntimeFailure, StreamCapability, StreamEvent,
21};
22use serde::{Deserialize, Serialize};
23use serde_json::Value;
24use sha2::{Digest, Sha256};
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct InstanceResources {
29 digest: String,
30 total_size: u64,
31 files: BTreeMap<String, Arc<[u8]>>,
32}
33
34impl Default for InstanceResources {
35 fn default() -> Self {
36 Self::from_files([]).expect("the empty resource snapshot is valid")
37 }
38}
39
40impl InstanceResources {
41 pub fn from_files(
43 files: impl IntoIterator<Item = (String, Vec<u8>)>,
44 ) -> Result<Self, RuntimeFailure> {
45 let mut indexed = BTreeMap::<String, Arc<[u8]>>::new();
46 let mut total_size = 0_u64;
47 for (path, bytes) in files {
48 validate_resource_path(&path)?;
49 total_size = total_size
50 .checked_add(
51 u64::try_from(bytes.len())
52 .map_err(|_| invalid_resources("resource file is too large"))?,
53 )
54 .ok_or_else(|| invalid_resources("resource snapshot size overflow"))?;
55 if indexed.insert(path.clone(), Arc::from(bytes)).is_some() {
56 return Err(invalid_resources(format!(
57 "duplicate Plugin resource path `{path}`"
58 )));
59 }
60 }
61 let mut hasher = Sha256::new();
62 hasher.update(b"lenso.instance-resources@1\0");
63 for (path, bytes) in &indexed {
64 hasher.update(
65 u64::try_from(path.len())
66 .expect("path length fits u64")
67 .to_be_bytes(),
68 );
69 hasher.update(path.as_bytes());
70 hasher.update(
71 u64::try_from(bytes.len())
72 .expect("content length fits u64")
73 .to_be_bytes(),
74 );
75 hasher.update(bytes.as_ref());
76 }
77 Ok(Self {
78 digest: format!("sha256:{}", hex::encode(hasher.finalize())),
79 total_size,
80 files: indexed,
81 })
82 }
83
84 pub fn digest(&self) -> &str {
86 &self.digest
87 }
88
89 pub fn file_count(&self) -> usize {
91 self.files.len()
92 }
93
94 pub const fn total_size(&self) -> u64 {
96 self.total_size
97 }
98
99 pub fn paths(&self) -> impl Iterator<Item = &str> {
101 self.files.keys().map(String::as_str)
102 }
103
104 pub fn read(&self, path: &str) -> Result<&[u8], RuntimeFailure> {
106 validate_resource_path(path)?;
107 self.files
108 .get(path)
109 .map(AsRef::as_ref)
110 .ok_or_else(|| invalid_resources(format!("Plugin resource `{path}` was not found")))
111 }
112
113 pub fn read_text(&self, path: &str) -> Result<&str, RuntimeFailure> {
115 std::str::from_utf8(self.read(path)?)
116 .map_err(|_| invalid_resources(format!("Plugin resource `{path}` is not UTF-8")))
117 }
118}
119
120#[derive(Clone, Debug, Default)]
122pub struct InstanceResourceCatalog {
123 snapshots: BTreeMap<String, InstanceResources>,
124 empty: InstanceResources,
125}
126
127impl InstanceResourceCatalog {
128 pub fn new() -> Self {
130 Self::default()
131 }
132
133 pub fn with_resources(
135 mut self,
136 instance_key: impl Into<String>,
137 resources: InstanceResources,
138 ) -> Result<Self, RuntimeFailure> {
139 let instance_key = instance_key.into();
140 if self
141 .snapshots
142 .insert(instance_key.clone(), resources)
143 .is_some()
144 {
145 return Err(invalid_resources(format!(
146 "duplicate resource authority for Instance `{instance_key}`"
147 )));
148 }
149 Ok(self)
150 }
151
152 pub fn for_instance(&self, instance_key: &str) -> &InstanceResources {
154 self.snapshots.get(instance_key).unwrap_or(&self.empty)
155 }
156
157 pub fn iter(&self) -> impl Iterator<Item = (&str, &InstanceResources)> {
159 self.snapshots
160 .iter()
161 .map(|(instance, resources)| (instance.as_str(), resources))
162 }
163}
164
165#[derive(Debug)]
167struct ArtifactBacking {
168 path: PathBuf,
169 _directory: tempfile::TempDir,
170}
171
172#[derive(Clone)]
174pub struct ArtifactHandle {
175 source_path: PathBuf,
176 backing: Arc<ArtifactBacking>,
177 digest: String,
178 size: u64,
179}
180
181impl std::fmt::Debug for ArtifactHandle {
182 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 formatter
184 .debug_struct("ArtifactHandle")
185 .field("source_path", &self.source_path)
186 .field("path", &self.backing.path)
187 .field("digest", &self.digest)
188 .field("size", &self.size)
189 .finish_non_exhaustive()
190 }
191}
192
193impl PartialEq for ArtifactHandle {
194 fn eq(&self, other: &Self) -> bool {
195 self.source_path == other.source_path
196 && self.digest == other.digest
197 && self.size == other.size
198 }
199}
200
201impl Eq for ArtifactHandle {}
202
203impl ArtifactHandle {
204 pub fn open(
209 path: impl Into<PathBuf>,
210 expected_digest: &str,
211 expected_size: u64,
212 ) -> Result<Self, RuntimeFailure> {
213 Self::open_inner(path.into(), expected_digest, expected_size, None)
214 }
215
216 pub fn open_with_staging_root(
219 path: impl Into<PathBuf>,
220 expected_digest: &str,
221 expected_size: u64,
222 staging_root: impl AsRef<Path>,
223 ) -> Result<Self, RuntimeFailure> {
224 Self::open_inner(
225 path.into(),
226 expected_digest,
227 expected_size,
228 Some(staging_root.as_ref()),
229 )
230 }
231
232 fn open_inner(
233 path: PathBuf,
234 expected_digest: &str,
235 expected_size: u64,
236 staging_root: Option<&Path>,
237 ) -> Result<Self, RuntimeFailure> {
238 validate_digest(expected_digest)?;
239 let path = absolute_path(path)?;
240 let metadata =
241 fs::symlink_metadata(&path).map_err(|error| invalid_artifact(&path, error))?;
242 if !metadata.file_type().is_file() || metadata.file_type().is_symlink() {
243 return Err(RuntimeFailure::InvalidResolvedPlan {
244 detail: format!("Artifact `{}` is not a regular file", path.display()),
245 });
246 }
247 if metadata.len() != expected_size {
248 return Err(RuntimeFailure::InvalidResolvedPlan {
249 detail: format!(
250 "Artifact `{}` size mismatch: expected {expected_size}, got {}",
251 path.display(),
252 metadata.len()
253 ),
254 });
255 }
256 let mut source = fs::File::open(&path).map_err(|error| invalid_artifact(&path, error))?;
257 let opened_metadata = source
258 .metadata()
259 .map_err(|error| invalid_artifact(&path, error))?;
260 if !opened_metadata.is_file() || opened_metadata.len() != expected_size {
261 return Err(RuntimeFailure::InvalidResolvedPlan {
262 detail: format!("Artifact `{}` changed during admission", path.display()),
263 });
264 }
265 let (backing, actual_digest, actual_size) =
266 materialize_stable_artifact(&path, &mut source, &opened_metadata, staging_root)?;
267 if actual_size != expected_size {
268 return Err(RuntimeFailure::InvalidResolvedPlan {
269 detail: format!("Artifact `{}` changed during admission", path.display()),
270 });
271 }
272 if actual_digest != expected_digest {
273 return Err(RuntimeFailure::InvalidResolvedPlan {
274 detail: format!("Artifact `{}` digest mismatch", path.display()),
275 });
276 }
277 Ok(Self {
278 source_path: path,
279 backing: Arc::new(backing),
280 digest: actual_digest,
281 size: opened_metadata.len(),
282 })
283 }
284
285 pub fn path(&self) -> &Path {
288 &self.backing.path
289 }
290
291 pub fn source_path(&self) -> &Path {
293 &self.source_path
294 }
295
296 pub fn digest(&self) -> &str {
298 &self.digest
299 }
300
301 pub const fn size(&self) -> u64 {
303 self.size
304 }
305
306 pub fn read_verified(&self) -> Result<Vec<u8>, RuntimeFailure> {
308 let bytes = fs::read(&self.backing.path)
309 .map_err(|error| invalid_artifact(&self.backing.path, error))?;
310 let size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
311 let digest = format!("sha256:{}", hex::encode(Sha256::digest(&bytes)));
312 if size != self.size || digest != self.digest {
313 return Err(RuntimeFailure::InvalidResolvedPlan {
314 detail: format!(
315 "stable Artifact `{}` changed after admission",
316 self.backing.path.display()
317 ),
318 });
319 }
320 Ok(bytes)
321 }
322}
323
324fn absolute_path(path: PathBuf) -> Result<PathBuf, RuntimeFailure> {
325 if path.is_absolute() {
326 return Ok(path);
327 }
328 env::current_dir()
329 .map(|current| current.join(path))
330 .map_err(|error| invalid_artifact(Path::new("."), error))
331}
332
333fn materialize_stable_artifact(
334 source_path: &Path,
335 source: &mut fs::File,
336 source_metadata: &fs::Metadata,
337 staging_root: Option<&Path>,
338) -> Result<(ArtifactBacking, String, u64), RuntimeFailure> {
339 let directory = stable_artifact_directory(source_path, staging_root)?;
340 let file_name = source_path
341 .file_name()
342 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
343 detail: format!("Artifact `{}` has no filename", source_path.display()),
344 })?;
345 let stable_path = directory.path().join(file_name);
346 let mut stable = fs::OpenOptions::new()
347 .create_new(true)
348 .write(true)
349 .open(&stable_path)
350 .map_err(|error| invalid_artifact(source_path, error))?;
351 let mut hasher = Sha256::new();
352 let mut size = 0_u64;
353 let mut buffer = vec![0_u8; 64 * 1024];
354 loop {
355 let read = std::io::Read::read(source, &mut buffer)
356 .map_err(|error| invalid_artifact(source_path, error))?;
357 if read == 0 {
358 break;
359 }
360 size = size
361 .checked_add(u64::try_from(read).expect("buffer length fits u64"))
362 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
363 detail: format!("Artifact `{}` size overflow", source_path.display()),
364 })?;
365 hasher.update(&buffer[..read]);
366 stable
367 .write_all(&buffer[..read])
368 .map_err(|error| invalid_artifact(source_path, error))?;
369 }
370 set_stable_permissions(&stable_path, source_metadata)
371 .map_err(|error| invalid_artifact(source_path, error))?;
372 Ok((
373 ArtifactBacking {
374 path: stable_path,
375 _directory: directory,
376 },
377 format!("sha256:{}", hex::encode(hasher.finalize())),
378 size,
379 ))
380}
381
382fn stable_artifact_directory(
383 source_path: &Path,
384 staging_root: Option<&Path>,
385) -> Result<tempfile::TempDir, RuntimeFailure> {
386 let builder = || {
387 let mut builder = tempfile::Builder::new();
388 builder.prefix("lenso-artifact-");
389 builder
390 };
391 if let Some(root) = staging_root {
392 return builder()
393 .tempdir_in(root)
394 .map_err(|error| invalid_artifact(source_path, error));
395 }
396 builder()
397 .tempdir()
398 .map_err(|error| invalid_artifact(source_path, error))
399}
400
401#[cfg(unix)]
402fn set_stable_permissions(path: &Path, metadata: &fs::Metadata) -> std::io::Result<()> {
403 use std::os::unix::fs::PermissionsExt as _;
404
405 let mode = metadata.permissions().mode() & 0o555;
406 fs::set_permissions(path, fs::Permissions::from_mode(mode))
407}
408
409#[cfg(not(unix))]
410fn set_stable_permissions(path: &Path, metadata: &fs::Metadata) -> std::io::Result<()> {
411 let mut permissions = metadata.permissions();
412 permissions.set_readonly(true);
413 fs::set_permissions(path, permissions)
414}
415
416#[derive(Clone, Debug, Default)]
418pub struct ArtifactCatalog(BTreeMap<String, ArtifactHandle>);
419
420impl ArtifactCatalog {
421 pub fn new() -> Self {
423 Self::default()
424 }
425
426 pub fn with_artifact(
428 mut self,
429 instance_key: impl Into<String>,
430 artifact: ArtifactHandle,
431 ) -> Result<Self, RuntimeFailure> {
432 let instance_key = instance_key.into();
433 if self.0.insert(instance_key.clone(), artifact).is_some() {
434 return Err(RuntimeFailure::InvalidResolvedPlan {
435 detail: format!("duplicate Artifact authority for Instance `{instance_key}`"),
436 });
437 }
438 Ok(self)
439 }
440
441 pub fn require(&self, instance_key: &str) -> Result<&ArtifactHandle, RuntimeFailure> {
443 self.0
444 .get(instance_key)
445 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
446 detail: format!("no admitted Artifact for Instance `{instance_key}`"),
447 })
448 }
449}
450
451pub trait JsonCapabilityCodec: std::fmt::Debug + 'static {
453 fn capability_id(&self) -> &'static str;
455 fn descriptor_version(&self) -> &'static str;
457 fn request_operations(&self) -> &'static [&'static str];
459 fn stream_operations(&self) -> &'static [&'static str] {
461 &[]
462 }
463 fn encode_request(&self, operation: &str, request: &dyn Any) -> Result<Value, RuntimeFailure>;
465 fn decode_response(
467 &self,
468 operation: &str,
469 value: Value,
470 ) -> Result<Box<dyn Any>, RuntimeFailure>;
471 fn decode_domain_error(
473 &self,
474 operation: &str,
475 value: Value,
476 ) -> Result<Box<dyn Any>, RuntimeFailure>;
477 fn encode_stream_open(
479 &self,
480 operation: &str,
481 request: &dyn Any,
482 ) -> Result<Value, RuntimeFailure> {
483 let _ = request;
484 Err(unknown_operation(self.capability_id(), operation))
485 }
486 fn encode_stream_message(
488 &self,
489 operation: &str,
490 message: &dyn Any,
491 ) -> Result<Value, RuntimeFailure> {
492 let _ = message;
493 Err(unknown_operation(self.capability_id(), operation))
494 }
495 fn decode_stream_message(
497 &self,
498 operation: &str,
499 value: Value,
500 ) -> Result<Box<dyn Any>, RuntimeFailure> {
501 let _ = value;
502 Err(unknown_operation(self.capability_id(), operation))
503 }
504 fn decode_stream_domain_error(
506 &self,
507 operation: &str,
508 value: Value,
509 ) -> Result<Box<dyn Any>, RuntimeFailure> {
510 let _ = value;
511 Err(unknown_operation(self.capability_id(), operation))
512 }
513 fn invoke_host_request(
515 &self,
516 dependency: PluginDependencyHandle,
517 operation: String,
518 request: Value,
519 context: InvocationContext,
520 ) -> JsonHostRequestFuture {
521 let _ = (dependency, request, context);
522 Box::pin(futures::future::ready(Err(unknown_operation(
523 self.capability_id(),
524 &operation,
525 ))))
526 }
527 fn open_host_stream(
529 &self,
530 dependency: PluginStreamDependencyHandle,
531 operation: String,
532 request: Value,
533 context: InvocationContext,
534 ) -> JsonHostStreamOpenFuture {
535 let _ = (dependency, request, context);
536 Box::pin(futures::future::ready(Err(unknown_operation(
537 self.capability_id(),
538 &operation,
539 ))))
540 }
541}
542
543#[derive(Debug)]
545pub enum JsonInvocationOutcome {
546 Success(Value),
548 DomainError(Value),
550}
551
552pub fn json_runtime_failure(error: &RuntimeFailure) -> Value {
554 match error {
555 RuntimeFailure::Unavailable { capability } => serde_json::json!({
556 "kind": "unavailable",
557 "capability": capability,
558 }),
559 RuntimeFailure::UnknownOperation {
560 capability,
561 operation,
562 } => serde_json::json!({
563 "kind": "unknown_operation",
564 "capability": capability,
565 "operation": operation,
566 }),
567 RuntimeFailure::AmbiguousBinding {
568 capability,
569 providers,
570 } => serde_json::json!({
571 "kind": "ambiguous_binding",
572 "capability": capability,
573 "providers": providers,
574 }),
575 RuntimeFailure::ProtocolViolation { capability } => serde_json::json!({
576 "kind": "protocol_violation",
577 "capability": capability,
578 }),
579 RuntimeFailure::AdmissionClosed => serde_json::json!({ "kind": "admission_closed" }),
580 RuntimeFailure::ResourceExhausted {
581 capability,
582 operation,
583 } => serde_json::json!({
584 "kind": "resource_exhausted",
585 "capability": capability,
586 "operation": operation,
587 }),
588 RuntimeFailure::DeadlineExceeded { request_id } => serde_json::json!({
589 "kind": "deadline_exceeded",
590 "request_id": request_id.to_string(),
591 }),
592 RuntimeFailure::Cancelled { request_id } => serde_json::json!({
593 "kind": "cancelled",
594 "request_id": request_id.to_string(),
595 }),
596 RuntimeFailure::MissingPluginFactory { .. }
597 | RuntimeFailure::UnavailableExecutionClass { .. }
598 | RuntimeFailure::InvalidResolvedPlan { .. }
599 | RuntimeFailure::Internal { .. }
600 | RuntimeFailure::PluginFailure { .. }
601 | RuntimeFailure::PluginRestartExhausted { .. } => {
602 serde_json::json!({ "kind": "internal" })
603 }
604 }
605}
606
607pub fn json_host_invocation_envelope(
609 outcome: Result<JsonInvocationOutcome, RuntimeFailure>,
610) -> Value {
611 match outcome {
612 Ok(JsonInvocationOutcome::Success(value)) => serde_json::json!({ "ok": value }),
613 Ok(JsonInvocationOutcome::DomainError(value)) => serde_json::json!({ "error": value }),
614 Err(error) => serde_json::json!({ "runtime": json_runtime_failure(&error) }),
615 }
616}
617
618pub type JsonHostRequestFuture =
620 futures::future::LocalBoxFuture<'static, Result<JsonInvocationOutcome, RuntimeFailure>>;
621
622pub trait JsonHostStreamSession: std::fmt::Debug + 'static {
624 fn send(
625 self: Rc<Self>,
626 message: Value,
627 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
628 fn receive(
629 self: Rc<Self>,
630 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>>;
631 fn close_send(
632 self: Rc<Self>,
633 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
634 fn cancel(&self);
635}
636
637pub type JsonHostStreamOpenFuture = futures::future::LocalBoxFuture<
639 'static,
640 Result<Result<Rc<dyn JsonHostStreamSession>, Value>, RuntimeFailure>,
641>;
642
643type DecodeStreamMessage<C> =
644 Rc<dyn Fn(Value) -> Result<<C as StreamCapability>::Message, RuntimeFailure>>;
645type EncodeStreamMessage<C> =
646 Rc<dyn Fn(<C as StreamCapability>::Message) -> Result<Value, RuntimeFailure>>;
647type EncodeStreamError<C> =
648 Rc<dyn Fn(<C as StreamCapability>::DomainError) -> Result<Value, RuntimeFailure>>;
649
650pub fn json_host_stream<C: StreamCapability>(
652 stream: NativeStream<C>,
653 decode_message: impl Fn(Value) -> Result<C::Message, RuntimeFailure> + 'static,
654 encode_message: impl Fn(C::Message) -> Result<Value, RuntimeFailure> + 'static,
655 encode_error: impl Fn(C::DomainError) -> Result<Value, RuntimeFailure> + 'static,
656) -> Rc<dyn JsonHostStreamSession> {
657 Rc::new(TypedJsonHostStream {
658 stream: Rc::new(stream),
659 decode_message: Rc::new(decode_message),
660 encode_message: Rc::new(encode_message),
661 encode_error: Rc::new(encode_error),
662 })
663}
664
665struct TypedJsonHostStream<C: StreamCapability> {
666 stream: Rc<NativeStream<C>>,
667 decode_message: DecodeStreamMessage<C>,
668 encode_message: EncodeStreamMessage<C>,
669 encode_error: EncodeStreamError<C>,
670}
671
672impl<C: StreamCapability> std::fmt::Debug for TypedJsonHostStream<C> {
673 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
674 formatter
675 .debug_struct("TypedJsonHostStream")
676 .field("capability", &C::ID)
677 .finish_non_exhaustive()
678 }
679}
680
681impl<C: StreamCapability> JsonHostStreamSession for TypedJsonHostStream<C> {
682 fn send(
683 self: Rc<Self>,
684 message: Value,
685 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
686 Box::pin(async move {
687 let message = (self.decode_message)(message)?;
688 self.stream.send(message).await
689 })
690 }
691
692 fn receive(
693 self: Rc<Self>,
694 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>> {
695 Box::pin(async move {
696 match self.stream.receive().await? {
697 StreamEvent::Message(message) => {
698 (self.encode_message)(message).map(JsonStreamItem::Message)
699 }
700 StreamEvent::PeerHalfClosed => Ok(JsonStreamItem::PeerHalfClosed),
701 StreamEvent::Terminal(Ok(())) => Ok(JsonStreamItem::Terminal(Ok(()))),
702 StreamEvent::Terminal(Err(error)) => {
703 (self.encode_error)(error).map(|error| JsonStreamItem::Terminal(Err(error)))
704 }
705 }
706 })
707 }
708
709 fn close_send(
710 self: Rc<Self>,
711 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
712 Box::pin(async move { self.stream.close_send().await })
713 }
714
715 fn cancel(&self) {
716 self.stream.cancel();
717 }
718}
719
720pub const JSON_REQUEST_ABI_V1: &str = "lenso.json-request@1";
722
723pub const JSON_INTERACTIONS_ABI_V1: &str = "lenso.json-interactions@1";
725
726pub const JSON_HOST_IMPORTS_ABI_V1: &str = "lenso.json-host-imports@1";
728pub const JSON_HOST_IMPORTS_ABI_V2: &str = "lenso.json-host-imports@2";
730
731#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
733#[serde(deny_unknown_fields)]
734pub struct JsonPluginDescriptor {
735 pub abi: String,
736 pub capabilities: Vec<JsonCapabilityDescriptor>,
737 #[serde(default, skip_serializing_if = "Vec::is_empty")]
738 pub required_capabilities: Vec<JsonRequiredCapabilityDescriptor>,
739}
740
741#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
743#[serde(deny_unknown_fields)]
744pub struct JsonCapabilityDescriptor {
745 pub capability_id: String,
746 pub descriptor_version: String,
747 pub request_operations: Vec<String>,
748 #[serde(default, skip_serializing_if = "Vec::is_empty")]
749 pub stream_operations: Vec<String>,
750}
751
752#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
754#[serde(deny_unknown_fields)]
755pub struct JsonRequiredCapabilityDescriptor {
756 pub requirement_id: String,
757 pub capability_id: String,
758 pub descriptor_version: String,
759 pub cardinality: CapabilityCardinality,
760}
761
762pub fn expected_json_plugin_descriptor(
764 instance: &PluginInstancePlan,
765) -> Result<JsonPluginDescriptor, RuntimeFailure> {
766 let mut capabilities = Vec::with_capacity(instance.provided_capabilities().len());
767 for descriptor in instance.provided_capabilities() {
768 if !descriptor.event_operations().is_empty() {
769 return Err(RuntimeFailure::InvalidResolvedPlan {
770 detail: format!(
771 "Execution class `{}` does not support Event endpoints",
772 instance.execution_class()
773 ),
774 });
775 }
776 capabilities.push(JsonCapabilityDescriptor {
777 capability_id: descriptor.capability_id().to_owned(),
778 descriptor_version: descriptor.descriptor_version().to_owned(),
779 request_operations: descriptor
780 .request_operations()
781 .into_iter()
782 .map(str::to_owned)
783 .collect(),
784 stream_operations: descriptor
785 .stream_operations()
786 .into_iter()
787 .map(str::to_owned)
788 .collect(),
789 });
790 }
791 capabilities.sort();
792 if capabilities
793 .windows(2)
794 .any(|pair| pair[0].capability_id == pair[1].capability_id)
795 {
796 return Err(RuntimeFailure::InvalidResolvedPlan {
797 detail: format!(
798 "Instance `{}` declares a duplicate Capability",
799 instance.instance_key()
800 ),
801 });
802 }
803 let mut required_capabilities = instance
804 .required_capabilities()
805 .iter()
806 .map(|requirement| JsonRequiredCapabilityDescriptor {
807 requirement_id: requirement.requirement_id().to_owned(),
808 capability_id: requirement.capability_id().to_owned(),
809 descriptor_version: requirement.descriptor_version().to_owned(),
810 cardinality: requirement.cardinality(),
811 })
812 .collect::<Vec<_>>();
813 sort_required_capabilities(&mut required_capabilities);
814 Ok(JsonPluginDescriptor {
815 abi: if !required_capabilities.is_empty() {
816 JSON_HOST_IMPORTS_ABI_V2
817 } else if capabilities
818 .iter()
819 .any(|capability| !capability.stream_operations.is_empty())
820 {
821 JSON_INTERACTIONS_ABI_V1
822 } else {
823 JSON_REQUEST_ABI_V1
824 }
825 .to_owned(),
826 capabilities,
827 required_capabilities,
828 })
829}
830
831pub fn validate_json_plugin_descriptor(
833 instance: &PluginInstancePlan,
834 encoded: &str,
835) -> Result<(), RuntimeFailure> {
836 let mut actual = serde_json::from_str::<JsonPluginDescriptor>(encoded).map_err(|_| {
837 RuntimeFailure::ProtocolViolation {
838 capability: "lenso.json-request@1",
839 }
840 })?;
841 actual.capabilities.sort();
842 sort_required_capabilities(&mut actual.required_capabilities);
843 let expected = expected_json_plugin_descriptor(instance)?;
844 if actual != expected {
845 return Err(RuntimeFailure::InvalidResolvedPlan {
846 detail: format!(
847 "guest descriptor does not match resolved Instance `{}`",
848 instance.instance_key()
849 ),
850 });
851 }
852 Ok(())
853}
854
855fn sort_required_capabilities(requirements: &mut [JsonRequiredCapabilityDescriptor]) {
856 requirements.sort_by(|left, right| {
857 (
858 &left.requirement_id,
859 &left.capability_id,
860 &left.descriptor_version,
861 cardinality_order(left.cardinality),
862 )
863 .cmp(&(
864 &right.requirement_id,
865 &right.capability_id,
866 &right.descriptor_version,
867 cardinality_order(right.cardinality),
868 ))
869 });
870}
871
872const fn cardinality_order(cardinality: CapabilityCardinality) -> u8 {
873 match cardinality {
874 CapabilityCardinality::One => 0,
875 CapabilityCardinality::Optional => 1,
876 CapabilityCardinality::Many => 2,
877 }
878}
879
880pub trait JsonRequestTransport: std::fmt::Debug + 'static {
882 fn invoke(
883 self: Rc<Self>,
884 capability: String,
885 operation: String,
886 request_json: String,
887 context: InvocationContext,
888 ) -> futures::future::LocalBoxFuture<'static, Result<JsonInvocationOutcome, RuntimeFailure>>;
889}
890
891#[derive(Debug)]
893pub enum JsonStreamItem {
894 Message(Value),
895 PeerHalfClosed,
896 Terminal(Result<(), Value>),
897}
898
899#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
901#[serde(
902 tag = "kind",
903 content = "value",
904 rename_all = "kebab-case",
905 deny_unknown_fields
906)]
907pub enum JsonStreamFrame {
908 Message(Value),
909 PeerHalfClosed,
910 TerminalSuccess,
911 TerminalError(Value),
912}
913
914#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
916pub struct JsonHostBindingDescriptor {
917 pub binding_id: u32,
918 pub requirement_id: String,
919 pub provider_instance: String,
920 pub capability_id: String,
921 pub descriptor_version: String,
922 pub request_operations: Vec<String>,
923 pub stream_operations: Vec<String>,
924}
925
926#[derive(Clone)]
927struct JsonHostBinding {
928 descriptor: JsonHostBindingDescriptor,
929 codec: Rc<dyn JsonCapabilityCodec>,
930 request: Option<PluginDependencyHandle>,
931 stream: Option<PluginStreamDependencyHandle>,
932}
933
934impl std::fmt::Debug for JsonHostBinding {
935 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
936 formatter
937 .debug_struct("JsonHostBinding")
938 .field("descriptor", &self.descriptor)
939 .finish_non_exhaustive()
940 }
941}
942
943#[derive(Debug)]
945pub struct JsonHostImports {
946 codecs: BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
947 bindings: std::cell::RefCell<Option<Vec<JsonHostBinding>>>,
948 streams: std::cell::RefCell<BTreeMap<u64, Rc<dyn JsonHostStreamSession>>>,
949 next_stream_id: std::cell::Cell<u64>,
950 max_streams: usize,
951}
952
953impl JsonHostImports {
954 pub fn new(
956 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
957 max_streams: usize,
958 ) -> Result<Self, RuntimeFailure> {
959 let mut by_capability = BTreeMap::new();
960 for codec in codecs {
961 let capability = codec.capability_id().to_owned();
962 if let Some(existing) = by_capability.get(&capability) {
963 if !Rc::ptr_eq(existing, &codec) {
964 return Err(RuntimeFailure::InvalidResolvedPlan {
965 detail: format!(
966 "conflicting guest import codecs for Capability `{capability}`"
967 ),
968 });
969 }
970 } else {
971 by_capability.insert(capability, codec);
972 }
973 }
974 Ok(Self {
975 codecs: by_capability,
976 bindings: std::cell::RefCell::new(None),
977 streams: std::cell::RefCell::new(BTreeMap::new()),
978 next_stream_id: std::cell::Cell::new(1),
979 max_streams,
980 })
981 }
982
983 pub fn activate(&self, dependencies: &PluginDependencies) -> Result<(), RuntimeFailure> {
985 if self.bindings.borrow().is_some() {
986 return Err(RuntimeFailure::Internal {
987 detail: "guest Capability imports were activated twice".to_owned(),
988 });
989 }
990 let mut bindings = Vec::with_capacity(dependencies.len());
991 for (index, dependency) in dependencies.bindings().iter().enumerate() {
992 let codec = self
993 .codecs
994 .get(dependency.capability_id())
995 .cloned()
996 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
997 detail: format!(
998 "no generated guest import codec for Capability `{}`",
999 dependency.capability_id()
1000 ),
1001 })?;
1002 let request = dependency.handle();
1003 let stream = dependency.stream_handle();
1004 validate_host_binding(&codec, request.as_ref(), stream.as_ref())?;
1005 let binding_id =
1006 u32::try_from(index).map_err(|_| RuntimeFailure::InvalidResolvedPlan {
1007 detail: "guest import binding table exceeds u32 identity space".to_owned(),
1008 })?;
1009 bindings.push(JsonHostBinding {
1010 descriptor: JsonHostBindingDescriptor {
1011 binding_id,
1012 requirement_id: dependency.requirement_id().to_owned(),
1013 provider_instance: dependency.provider_instance().to_owned(),
1014 capability_id: dependency.capability_id().to_owned(),
1015 descriptor_version: codec.descriptor_version().to_owned(),
1016 request_operations: request.as_ref().map_or_else(Vec::new, |handle| {
1017 handle
1018 .operations()
1019 .iter()
1020 .map(|item| (*item).to_owned())
1021 .collect()
1022 }),
1023 stream_operations: stream.as_ref().map_or_else(Vec::new, |handle| {
1024 handle
1025 .operations()
1026 .iter()
1027 .map(|item| (*item).to_owned())
1028 .collect()
1029 }),
1030 },
1031 codec,
1032 request,
1033 stream,
1034 });
1035 }
1036 self.bindings.replace(Some(bindings));
1037 Ok(())
1038 }
1039
1040 pub fn descriptors(&self) -> Result<Vec<JsonHostBindingDescriptor>, RuntimeFailure> {
1042 self.bindings
1043 .borrow()
1044 .as_ref()
1045 .map(|bindings| {
1046 bindings
1047 .iter()
1048 .map(|binding| binding.descriptor.clone())
1049 .collect()
1050 })
1051 .ok_or(RuntimeFailure::AdmissionClosed)
1052 }
1053
1054 pub fn invoke(
1056 &self,
1057 binding_id: u32,
1058 operation: String,
1059 request: Value,
1060 context: InvocationContext,
1061 ) -> JsonHostRequestFuture {
1062 let binding = match self.binding(binding_id) {
1063 Ok(binding) => binding,
1064 Err(error) => return Box::pin(futures::future::ready(Err(error))),
1065 };
1066 let Some(dependency) = binding.request else {
1067 return Box::pin(futures::future::ready(Err(
1068 RuntimeFailure::UnknownOperation {
1069 capability: binding.codec.capability_id(),
1070 operation,
1071 },
1072 )));
1073 };
1074 binding
1075 .codec
1076 .invoke_host_request(dependency, operation, request, context)
1077 }
1078
1079 pub fn open_stream(
1081 self: Rc<Self>,
1082 binding_id: u32,
1083 operation: String,
1084 request: Value,
1085 context: InvocationContext,
1086 ) -> futures::future::LocalBoxFuture<'static, Result<Result<u64, Value>, RuntimeFailure>> {
1087 Box::pin(async move {
1088 if self.streams.borrow().len() >= self.max_streams {
1089 return Err(RuntimeFailure::ResourceExhausted {
1090 capability: JSON_HOST_IMPORTS_ABI_V2,
1091 operation: "stream-open".to_owned(),
1092 });
1093 }
1094 let binding = self.binding(binding_id)?;
1095 let dependency = binding
1096 .stream
1097 .ok_or_else(|| RuntimeFailure::UnknownOperation {
1098 capability: binding.codec.capability_id(),
1099 operation: operation.clone(),
1100 })?;
1101 match binding
1102 .codec
1103 .open_host_stream(dependency, operation, request, context)
1104 .await?
1105 {
1106 Ok(stream) => {
1107 let stream_id = self.next_stream_id.get();
1108 let next =
1109 stream_id
1110 .checked_add(1)
1111 .ok_or(RuntimeFailure::ResourceExhausted {
1112 capability: JSON_HOST_IMPORTS_ABI_V2,
1113 operation: "stream-open".to_owned(),
1114 })?;
1115 self.next_stream_id.set(next);
1116 self.streams.borrow_mut().insert(stream_id, stream);
1117 Ok(Ok(stream_id))
1118 }
1119 Err(error) => Ok(Err(error)),
1120 }
1121 })
1122 }
1123
1124 pub fn send_stream(
1126 &self,
1127 stream_id: u64,
1128 message: Value,
1129 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1130 match self.stream(stream_id) {
1131 Ok(stream) => stream.send(message),
1132 Err(error) => Box::pin(futures::future::ready(Err(error))),
1133 }
1134 }
1135
1136 pub fn receive_stream(
1138 self: Rc<Self>,
1139 stream_id: u64,
1140 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>> {
1141 Box::pin(async move {
1142 let stream = self.stream(stream_id)?;
1143 let item = stream.receive().await?;
1144 if matches!(item, JsonStreamItem::Terminal(_)) {
1145 self.streams.borrow_mut().remove(&stream_id);
1146 }
1147 Ok(item)
1148 })
1149 }
1150
1151 pub fn close_stream_send(
1153 &self,
1154 stream_id: u64,
1155 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1156 match self.stream(stream_id) {
1157 Ok(stream) => stream.close_send(),
1158 Err(error) => Box::pin(futures::future::ready(Err(error))),
1159 }
1160 }
1161
1162 pub fn cancel_stream(&self, stream_id: u64) -> Result<(), RuntimeFailure> {
1164 let stream = self
1165 .streams
1166 .borrow_mut()
1167 .remove(&stream_id)
1168 .ok_or_else(unknown_host_stream)?;
1169 stream.cancel();
1170 Ok(())
1171 }
1172
1173 pub fn deactivate(&self) {
1175 self.bindings.replace(None);
1176 for (_, stream) in std::mem::take(&mut *self.streams.borrow_mut()) {
1177 stream.cancel();
1178 }
1179 }
1180
1181 fn binding(&self, binding_id: u32) -> Result<JsonHostBinding, RuntimeFailure> {
1182 let bindings = self.bindings.borrow();
1183 let bindings = bindings.as_ref().ok_or(RuntimeFailure::AdmissionClosed)?;
1184 bindings
1185 .get(binding_id as usize)
1186 .cloned()
1187 .ok_or(RuntimeFailure::ProtocolViolation {
1188 capability: JSON_HOST_IMPORTS_ABI_V2,
1189 })
1190 }
1191
1192 fn stream(&self, stream_id: u64) -> Result<Rc<dyn JsonHostStreamSession>, RuntimeFailure> {
1193 self.streams
1194 .borrow()
1195 .get(&stream_id)
1196 .cloned()
1197 .ok_or_else(unknown_host_stream)
1198 }
1199}
1200
1201fn validate_host_binding(
1202 codec: &Rc<dyn JsonCapabilityCodec>,
1203 request: Option<&PluginDependencyHandle>,
1204 stream: Option<&PluginStreamDependencyHandle>,
1205) -> Result<(), RuntimeFailure> {
1206 for (capability, version) in request
1207 .map(|handle| (handle.capability_id(), handle.descriptor_version()))
1208 .into_iter()
1209 .chain(stream.map(|handle| (handle.capability_id(), handle.descriptor_version())))
1210 {
1211 if capability != codec.capability_id() || version != codec.descriptor_version() {
1212 return Err(RuntimeFailure::ProtocolViolation {
1213 capability: codec.capability_id(),
1214 });
1215 }
1216 }
1217 Ok(())
1218}
1219
1220fn unknown_host_stream() -> RuntimeFailure {
1221 RuntimeFailure::ProtocolViolation {
1222 capability: JSON_HOST_IMPORTS_ABI_V2,
1223 }
1224}
1225
1226impl JsonStreamFrame {
1227 pub fn decode(
1229 encoded: &str,
1230 capability: &'static str,
1231 ) -> Result<JsonStreamItem, RuntimeFailure> {
1232 match serde_json::from_str(encoded)
1233 .map_err(|_| RuntimeFailure::ProtocolViolation { capability })?
1234 {
1235 Self::Message(value) => Ok(JsonStreamItem::Message(value)),
1236 Self::PeerHalfClosed => Ok(JsonStreamItem::PeerHalfClosed),
1237 Self::TerminalSuccess => Ok(JsonStreamItem::Terminal(Ok(()))),
1238 Self::TerminalError(value) => Ok(JsonStreamItem::Terminal(Err(value))),
1239 }
1240 }
1241}
1242
1243pub trait JsonStreamSessionTransport: std::fmt::Debug + 'static {
1245 fn send(
1246 self: Rc<Self>,
1247 message_json: String,
1248 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
1249 fn receive(
1250 self: Rc<Self>,
1251 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>>;
1252 fn close_send(
1253 self: Rc<Self>,
1254 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
1255 fn cancel(&self);
1256}
1257
1258pub type JsonStreamOpenFuture = futures::future::LocalBoxFuture<
1260 'static,
1261 Result<Result<Rc<dyn JsonStreamSessionTransport>, Value>, RuntimeFailure>,
1262>;
1263
1264pub trait JsonStreamTransport: std::fmt::Debug + 'static {
1266 fn open(
1267 self: Rc<Self>,
1268 capability: String,
1269 operation: String,
1270 request_json: String,
1271 context: InvocationContext,
1272 ) -> JsonStreamOpenFuture;
1273}
1274
1275pub fn json_request_endpoints<T: JsonRequestTransport>(
1277 transport: Rc<T>,
1278 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
1279) -> Vec<Rc<dyn NativeRequestEndpoint>> {
1280 let transport: Rc<dyn JsonRequestTransport> = transport;
1281 codecs
1282 .into_iter()
1283 .filter(|codec| !codec.request_operations().is_empty())
1284 .map(|codec| {
1285 Rc::new(JsonRequestEndpoint {
1286 transport: transport.clone(),
1287 codec,
1288 }) as Rc<dyn NativeRequestEndpoint>
1289 })
1290 .collect()
1291}
1292
1293pub fn json_stream_endpoints<T: JsonStreamTransport>(
1295 transport: Rc<T>,
1296 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
1297) -> Vec<Rc<dyn NativeStreamEndpoint>> {
1298 let transport: Rc<dyn JsonStreamTransport> = transport;
1299 codecs
1300 .into_iter()
1301 .filter(|codec| !codec.stream_operations().is_empty())
1302 .map(|codec| {
1303 Rc::new(JsonStreamEndpoint {
1304 transport: transport.clone(),
1305 codec,
1306 }) as Rc<dyn NativeStreamEndpoint>
1307 })
1308 .collect()
1309}
1310
1311#[derive(Debug)]
1312struct JsonStreamEndpoint {
1313 transport: Rc<dyn JsonStreamTransport>,
1314 codec: Rc<dyn JsonCapabilityCodec>,
1315}
1316
1317impl NativeStreamEndpoint for JsonStreamEndpoint {
1318 fn capability_id(&self) -> &'static str {
1319 self.codec.capability_id()
1320 }
1321 fn descriptor_version(&self) -> &'static str {
1322 self.codec.descriptor_version()
1323 }
1324 fn operations(&self) -> &'static [&'static str] {
1325 self.codec.stream_operations()
1326 }
1327
1328 fn open(
1329 &self,
1330 operation: &str,
1331 request: Box<dyn Any>,
1332 context: InvocationContext,
1333 ) -> futures::future::LocalBoxFuture<
1334 'static,
1335 Result<Result<Box<dyn NativeStreamSession>, Box<dyn Any>>, RuntimeFailure>,
1336 > {
1337 let transport = self.transport.clone();
1338 let codec = self.codec.clone();
1339 let operation = operation.to_owned();
1340 Box::pin(async move {
1341 if !codec.stream_operations().contains(&operation.as_str()) {
1342 return Err(unknown_operation(codec.capability_id(), &operation));
1343 }
1344 let request = codec.encode_stream_open(&operation, request.as_ref())?;
1345 let request_json =
1346 serde_json::to_string(&request).map_err(|_| RuntimeFailure::ProtocolViolation {
1347 capability: codec.capability_id(),
1348 })?;
1349 match transport
1350 .open(
1351 codec.capability_id().to_owned(),
1352 operation.clone(),
1353 request_json,
1354 context,
1355 )
1356 .await?
1357 {
1358 Ok(session) => Ok(Ok(Box::new(JsonStreamSession {
1359 session,
1360 codec,
1361 operation,
1362 }) as Box<dyn NativeStreamSession>)),
1363 Err(error) => codec.decode_stream_domain_error(&operation, error).map(Err),
1364 }
1365 })
1366 }
1367}
1368
1369#[derive(Debug)]
1370struct JsonStreamSession {
1371 session: Rc<dyn JsonStreamSessionTransport>,
1372 codec: Rc<dyn JsonCapabilityCodec>,
1373 operation: String,
1374}
1375
1376impl NativeStreamSession for JsonStreamSession {
1377 fn send(
1378 &self,
1379 message: Box<dyn Any>,
1380 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1381 let encoded = self
1382 .codec
1383 .encode_stream_message(&self.operation, message.as_ref())
1384 .and_then(|value| {
1385 serde_json::to_string(&value).map_err(|_| RuntimeFailure::ProtocolViolation {
1386 capability: self.codec.capability_id(),
1387 })
1388 });
1389 let session = self.session.clone();
1390 Box::pin(async move { session.send(encoded?).await })
1391 }
1392
1393 fn receive(
1394 &self,
1395 ) -> futures::future::LocalBoxFuture<'static, Result<NativeStreamItem, RuntimeFailure>> {
1396 let session = self.session.clone();
1397 let codec = self.codec.clone();
1398 let operation = self.operation.clone();
1399 Box::pin(async move {
1400 match session.receive().await? {
1401 JsonStreamItem::Message(value) => codec
1402 .decode_stream_message(&operation, value)
1403 .map(NativeStreamItem::Message),
1404 JsonStreamItem::PeerHalfClosed => Ok(NativeStreamItem::PeerHalfClosed),
1405 JsonStreamItem::Terminal(Ok(())) => Ok(NativeStreamItem::Terminal(Ok(()))),
1406 JsonStreamItem::Terminal(Err(value)) => codec
1407 .decode_stream_domain_error(&operation, value)
1408 .map(|error| NativeStreamItem::Terminal(Err(error))),
1409 }
1410 })
1411 }
1412
1413 fn close_send(&self) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1414 self.session.clone().close_send()
1415 }
1416
1417 fn cancel(&self) {
1418 self.session.cancel();
1419 }
1420}
1421
1422#[derive(Debug)]
1423struct JsonRequestEndpoint {
1424 transport: Rc<dyn JsonRequestTransport>,
1425 codec: Rc<dyn JsonCapabilityCodec>,
1426}
1427
1428impl NativeRequestEndpoint for JsonRequestEndpoint {
1429 fn capability_id(&self) -> &'static str {
1430 self.codec.capability_id()
1431 }
1432
1433 fn descriptor_version(&self) -> &'static str {
1434 self.codec.descriptor_version()
1435 }
1436
1437 fn operations(&self) -> &'static [&'static str] {
1438 self.codec.request_operations()
1439 }
1440
1441 fn invoke(
1442 &self,
1443 operation: &str,
1444 request: Box<dyn Any>,
1445 context: InvocationContext,
1446 ) -> futures::future::LocalBoxFuture<
1447 'static,
1448 Result<Result<Box<dyn Any>, Box<dyn Any>>, RuntimeFailure>,
1449 > {
1450 let transport = self.transport.clone();
1451 let codec = self.codec.clone();
1452 let operation = operation.to_owned();
1453 Box::pin(async move {
1454 if !codec.request_operations().contains(&operation.as_str()) {
1455 return Err(RuntimeFailure::UnknownOperation {
1456 capability: codec.capability_id(),
1457 operation,
1458 });
1459 }
1460 let request = codec.encode_request(&operation, request.as_ref())?;
1461 let request =
1462 serde_json::to_string(&request).map_err(|_| RuntimeFailure::ProtocolViolation {
1463 capability: codec.capability_id(),
1464 })?;
1465 match transport
1466 .invoke(
1467 codec.capability_id().to_owned(),
1468 operation.clone(),
1469 request,
1470 context,
1471 )
1472 .await?
1473 {
1474 JsonInvocationOutcome::Success(value) => {
1475 codec.decode_response(&operation, value).map(Ok)
1476 }
1477 JsonInvocationOutcome::DomainError(value) => {
1478 codec.decode_domain_error(&operation, value).map(Err)
1479 }
1480 }
1481 })
1482 }
1483}
1484
1485pub fn codecs_for_instance(
1487 instance: &PluginInstancePlan,
1488 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1489) -> Result<Vec<Rc<dyn JsonCapabilityCodec>>, RuntimeFailure> {
1490 let mut selected = Vec::with_capacity(instance.provided_capabilities().len());
1491 for descriptor in instance.provided_capabilities() {
1492 if !descriptor.event_operations().is_empty() {
1493 return Err(RuntimeFailure::InvalidResolvedPlan {
1494 detail: format!(
1495 "Execution class `{}` does not support Event endpoints",
1496 instance.execution_class()
1497 ),
1498 });
1499 }
1500 let codec = codecs.get(descriptor.capability_id()).ok_or_else(|| {
1501 RuntimeFailure::InvalidResolvedPlan {
1502 detail: format!(
1503 "no generated codec for Capability `{}`",
1504 descriptor.capability_id()
1505 ),
1506 }
1507 })?;
1508 let request_operations: Vec<_> = codec
1509 .request_operations()
1510 .iter()
1511 .map(|operation| (*operation).to_owned())
1512 .collect();
1513 let stream_operations: Vec<_> = codec
1514 .stream_operations()
1515 .iter()
1516 .map(|operation| (*operation).to_owned())
1517 .collect();
1518 let expected_request: Vec<_> = descriptor
1519 .request_operations()
1520 .into_iter()
1521 .map(str::to_owned)
1522 .collect();
1523 let expected_stream: Vec<_> = descriptor
1524 .stream_operations()
1525 .into_iter()
1526 .map(str::to_owned)
1527 .collect();
1528 if codec.descriptor_version() != descriptor.descriptor_version()
1529 || request_operations != expected_request
1530 || stream_operations != expected_stream
1531 {
1532 return Err(RuntimeFailure::ProtocolViolation {
1533 capability: codec.capability_id(),
1534 });
1535 }
1536 selected.push(codec.clone());
1537 }
1538 Ok(selected)
1539}
1540
1541pub fn codecs_for_requirements(
1543 instance: &PluginInstancePlan,
1544 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1545) -> Result<Vec<Rc<dyn JsonCapabilityCodec>>, RuntimeFailure> {
1546 let mut selected = Vec::with_capacity(instance.required_capabilities().len());
1547 for requirement in instance.required_capabilities() {
1548 let codec = codecs.get(requirement.capability_id()).ok_or_else(|| {
1549 RuntimeFailure::InvalidResolvedPlan {
1550 detail: format!(
1551 "no generated guest import codec for Capability `{}`",
1552 requirement.capability_id()
1553 ),
1554 }
1555 })?;
1556 if codec.descriptor_version() != requirement.descriptor_version() {
1557 return Err(RuntimeFailure::ProtocolViolation {
1558 capability: codec.capability_id(),
1559 });
1560 }
1561 selected.push(codec.clone());
1562 }
1563 Ok(selected)
1564}
1565
1566pub fn prepare_request_app(
1568 plan: &ResolvedAppPlan,
1569 execution_class: &ExecutionClassId,
1570 generations: BTreeMap<String, PreparedNativePlugin>,
1571) -> Result<PreparedNativeApp, RuntimeFailure> {
1572 let selected_instances = plan
1573 .plugin_instances()
1574 .iter()
1575 .filter(|instance| instance.execution_class() == execution_class)
1576 .map(|instance| instance.instance_key().to_owned())
1577 .collect::<std::collections::BTreeSet<_>>();
1578 let mut endpoints = BTreeMap::new();
1579 let mut stream_endpoints = BTreeMap::new();
1580 for (instance_key, generation) in &generations {
1581 for endpoint in generation.endpoints() {
1582 let identity = (instance_key.clone(), endpoint.capability_id().to_owned());
1583 if endpoints.insert(identity, endpoint.clone()).is_some() {
1584 return Err(RuntimeFailure::InvalidResolvedPlan {
1585 detail: format!("duplicate request endpoint on Instance `{instance_key}`"),
1586 });
1587 }
1588 }
1589 for endpoint in generation.stream_endpoints() {
1590 let identity = (instance_key.clone(), endpoint.capability_id().to_owned());
1591 if stream_endpoints
1592 .insert(identity, endpoint.clone())
1593 .is_some()
1594 {
1595 return Err(RuntimeFailure::InvalidResolvedPlan {
1596 detail: format!("duplicate stream endpoint on Instance `{instance_key}`"),
1597 });
1598 }
1599 }
1600 }
1601 for instance in plan
1602 .plugin_instances()
1603 .iter()
1604 .filter(|instance| selected_instances.contains(instance.instance_key()))
1605 {
1606 if !generations.contains_key(instance.instance_key()) {
1607 return Err(RuntimeFailure::InvalidResolvedPlan {
1608 detail: format!("Adapter omitted Instance `{}`", instance.instance_key()),
1609 });
1610 }
1611 }
1612 let mut bindings = Vec::new();
1613 let mut stream_bindings = Vec::new();
1614 for binding in plan.capability_bindings() {
1615 let key = (
1616 binding.provider_instance().to_owned(),
1617 binding.capability_id().to_owned(),
1618 );
1619 let request_endpoint = endpoints.get(&key);
1620 let stream_endpoint = stream_endpoints.get(&key);
1621 if let Some(endpoint) = request_endpoint {
1622 bindings.push(
1623 PreparedBinding::new(
1624 binding.consumer_instance(),
1625 binding.provider_instance(),
1626 endpoint.clone(),
1627 )
1628 .with_requirement_id(binding.requirement_id()),
1629 );
1630 }
1631 if let Some(endpoint) = stream_endpoint {
1632 stream_bindings.push(
1633 PreparedStreamBinding::new(
1634 binding.consumer_instance(),
1635 binding.provider_instance(),
1636 endpoint.clone(),
1637 )
1638 .with_requirement_id(binding.requirement_id()),
1639 );
1640 }
1641 if request_endpoint.is_none()
1642 && stream_endpoint.is_none()
1643 && selected_instances.contains(binding.provider_instance())
1644 {
1645 return Err(RuntimeFailure::InvalidResolvedPlan {
1646 detail: format!(
1647 "Adapter omitted Capability `{}` endpoint for Instance `{}`",
1648 binding.capability_id(),
1649 binding.provider_instance()
1650 ),
1651 });
1652 }
1653 }
1654 Ok(PreparedNativeApp::new(bindings, generations).with_stream_bindings(stream_bindings))
1655}
1656
1657pub fn require_operation(
1659 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1660 capability_id: &str,
1661 operation: &str,
1662) -> Result<Rc<dyn JsonCapabilityCodec>, RuntimeFailure> {
1663 let codec =
1664 codecs
1665 .get(capability_id)
1666 .cloned()
1667 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
1668 detail: format!("no generated codec for Capability `{capability_id}`"),
1669 })?;
1670 if !codec.request_operations().contains(&operation) {
1671 return Err(RuntimeFailure::UnknownOperation {
1672 capability: codec.capability_id(),
1673 operation: operation.to_owned(),
1674 });
1675 }
1676 Ok(codec)
1677}
1678
1679fn unknown_operation(capability: &'static str, operation: &str) -> RuntimeFailure {
1680 RuntimeFailure::UnknownOperation {
1681 capability,
1682 operation: operation.to_owned(),
1683 }
1684}
1685
1686fn validate_digest(digest: &str) -> Result<(), RuntimeFailure> {
1687 let valid = digest.strip_prefix("sha256:").is_some_and(|hex| {
1688 hex.len() == 64
1689 && hex
1690 .bytes()
1691 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
1692 });
1693 if valid {
1694 Ok(())
1695 } else {
1696 Err(RuntimeFailure::InvalidResolvedPlan {
1697 detail: format!("invalid canonical SHA-256 digest `{digest}`"),
1698 })
1699 }
1700}
1701
1702fn invalid_artifact(path: &Path, error: impl std::fmt::Display) -> RuntimeFailure {
1703 RuntimeFailure::InvalidResolvedPlan {
1704 detail: format!("cannot read Artifact `{}`: {error}", path.display()),
1705 }
1706}
1707
1708fn validate_resource_path(path: &str) -> Result<(), RuntimeFailure> {
1709 if path.is_empty()
1710 || path.starts_with('/')
1711 || path.contains(['\\', '\0'])
1712 || path
1713 .split('/')
1714 .any(|segment| segment.is_empty() || matches!(segment, "." | ".."))
1715 {
1716 return Err(invalid_resources(format!(
1717 "invalid Plugin resource path `{path}`"
1718 )));
1719 }
1720 Ok(())
1721}
1722
1723fn invalid_resources(detail: impl Into<String>) -> RuntimeFailure {
1724 RuntimeFailure::InvalidResolvedPlan {
1725 detail: detail.into(),
1726 }
1727}
1728
1729#[cfg(test)]
1730mod tests {
1731 use std::io::Write;
1732
1733 use super::*;
1734
1735 #[test]
1736 fn artifact_handle_keeps_the_admitted_bytes_after_source_drift() {
1737 let mut file = tempfile::NamedTempFile::new().unwrap();
1738 file.write_all(b"first").unwrap();
1739 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"first")));
1740 let handle = ArtifactHandle::open(file.path(), &digest, 5).unwrap();
1741 file.as_file_mut().set_len(0).unwrap();
1742 file.write_all(b"other").unwrap();
1743 assert_eq!(handle.read_verified().unwrap(), b"first");
1744 assert_ne!(handle.path(), file.path());
1745 assert_eq!(fs::read(handle.path()).unwrap(), b"first");
1746 }
1747
1748 #[test]
1749 fn artifact_snapshot_survives_source_parent_rename_and_replacement() {
1750 let workspace = tempfile::tempdir().unwrap();
1751 let selected = workspace.path().join("selected");
1752 fs::create_dir(&selected).unwrap();
1753 let source = selected.join("plugin");
1754 fs::write(&source, b"admitted").unwrap();
1755 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"admitted")));
1756
1757 let handle = ArtifactHandle::open(&source, &digest, 8).unwrap();
1758 assert!(!handle.path().starts_with(&selected));
1759 fs::rename(&selected, workspace.path().join("replaced")).unwrap();
1760 fs::create_dir(&selected).unwrap();
1761 fs::write(selected.join("plugin"), b"attacker").unwrap();
1762
1763 assert_eq!(fs::read(handle.path()).unwrap(), b"admitted");
1764 assert_eq!(handle.read_verified().unwrap(), b"admitted");
1765 }
1766
1767 #[test]
1768 fn artifact_admission_streams_large_content_into_one_stable_snapshot() {
1769 let bytes = vec![0x5a; 4 * 1024 * 1024 + 17];
1770 let mut file = tempfile::NamedTempFile::new().unwrap();
1771 file.write_all(&bytes).unwrap();
1772 let digest = format!("sha256:{}", hex::encode(Sha256::digest(&bytes)));
1773
1774 let handle = ArtifactHandle::open(file.path(), &digest, bytes.len() as u64).unwrap();
1775
1776 assert_eq!(handle.read_verified().unwrap(), bytes);
1777 }
1778
1779 #[test]
1782 #[ignore = "large Artifact admission benchmark; run explicitly"]
1783 fn artifact_admission_streaming_benchmark() {
1784 const BLOCK_BYTES: usize = 64 * 1024;
1785 let block = vec![0x5a; BLOCK_BYTES];
1786 for mebibytes in [4_usize, 64, 256] {
1787 let directory = tempfile::tempdir().unwrap();
1788 let path = directory.path().join("artifact");
1789 let mut source = fs::File::create(&path).unwrap();
1790 let mut hasher = Sha256::new();
1791 let blocks = mebibytes * 1024 * 1024 / BLOCK_BYTES;
1792 for _ in 0..blocks {
1793 source.write_all(&block).unwrap();
1794 hasher.update(&block);
1795 }
1796 drop(source);
1797 let size = u64::try_from(mebibytes * 1024 * 1024).unwrap();
1798 let digest = format!("sha256:{}", hex::encode(hasher.finalize()));
1799
1800 let started = std::time::Instant::now();
1801 let handle = ArtifactHandle::open(&path, &digest, size).unwrap();
1802 let elapsed = started.elapsed();
1803
1804 assert_eq!(handle.size(), size);
1805 println!(
1806 "{{\"mebibytes\":{mebibytes},\"elapsed_ms\":{:.3},\"mib_per_second\":{:.3}}}",
1807 elapsed.as_secs_f64() * 1_000.0,
1808 f64::from(u32::try_from(mebibytes).unwrap()) / elapsed.as_secs_f64()
1809 );
1810 drop(handle);
1811 }
1812 }
1813
1814 #[test]
1815 fn artifact_admission_honors_an_explicit_host_staging_root() {
1816 let source = tempfile::tempdir().unwrap();
1817 let staging = tempfile::tempdir().unwrap();
1818 let path = source.path().join("plugin");
1819 fs::write(&path, b"artifact").unwrap();
1820 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"artifact")));
1821
1822 let handle =
1823 ArtifactHandle::open_with_staging_root(&path, &digest, 8, staging.path()).unwrap();
1824
1825 assert_eq!(
1826 handle.path().parent().unwrap().parent().unwrap(),
1827 staging.path()
1828 );
1829 }
1830
1831 #[test]
1832 fn instance_resources_are_order_independent_and_immutable() {
1833 let left = InstanceResources::from_files([
1834 ("prompts/system.md".to_owned(), b"Build carefully.".to_vec()),
1835 ("rules.toml".to_owned(), b"turns = 4\n".to_vec()),
1836 ])
1837 .unwrap();
1838 let right = InstanceResources::from_files([
1839 ("rules.toml".to_owned(), b"turns = 4\n".to_vec()),
1840 ("prompts/system.md".to_owned(), b"Build carefully.".to_vec()),
1841 ])
1842 .unwrap();
1843
1844 assert_eq!(left.digest(), right.digest());
1845 assert_eq!(
1846 left.read_text("prompts/system.md").unwrap(),
1847 "Build carefully."
1848 );
1849 assert_eq!(left.file_count(), 2);
1850 assert_eq!(left.total_size(), 26);
1851 }
1852
1853 #[test]
1854 fn instance_resources_reject_escaping_and_duplicate_paths() {
1855 assert!(InstanceResources::from_files([("../secret".to_owned(), Vec::new())]).is_err());
1856 assert!(
1857 InstanceResources::from_files([
1858 ("rules.toml".to_owned(), Vec::new()),
1859 ("rules.toml".to_owned(), Vec::new()),
1860 ])
1861 .is_err()
1862 );
1863 }
1864}