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 descriptor_digest(&self) -> &'static str {
463 ""
464 }
465 fn request_operations(&self) -> &'static [&'static str];
467 fn stream_operations(&self) -> &'static [&'static str] {
469 &[]
470 }
471 fn encode_request(&self, operation: &str, request: &dyn Any) -> Result<Value, RuntimeFailure>;
473 fn decode_response(
475 &self,
476 operation: &str,
477 value: Value,
478 ) -> Result<Box<dyn Any>, RuntimeFailure>;
479 fn decode_domain_error(
481 &self,
482 operation: &str,
483 value: Value,
484 ) -> Result<Box<dyn Any>, RuntimeFailure>;
485 fn encode_stream_open(
487 &self,
488 operation: &str,
489 request: &dyn Any,
490 ) -> Result<Value, RuntimeFailure> {
491 let _ = request;
492 Err(unknown_operation(self.capability_id(), operation))
493 }
494 fn encode_stream_message(
496 &self,
497 operation: &str,
498 message: &dyn Any,
499 ) -> Result<Value, RuntimeFailure> {
500 let _ = message;
501 Err(unknown_operation(self.capability_id(), operation))
502 }
503 fn decode_stream_message(
505 &self,
506 operation: &str,
507 value: Value,
508 ) -> Result<Box<dyn Any>, RuntimeFailure> {
509 let _ = value;
510 Err(unknown_operation(self.capability_id(), operation))
511 }
512 fn decode_stream_domain_error(
514 &self,
515 operation: &str,
516 value: Value,
517 ) -> Result<Box<dyn Any>, RuntimeFailure> {
518 let _ = value;
519 Err(unknown_operation(self.capability_id(), operation))
520 }
521 fn invoke_host_request(
523 &self,
524 dependency: PluginDependencyHandle,
525 operation: String,
526 request: Value,
527 context: InvocationContext,
528 ) -> JsonHostRequestFuture {
529 let _ = (dependency, request, context);
530 Box::pin(futures::future::ready(Err(unknown_operation(
531 self.capability_id(),
532 &operation,
533 ))))
534 }
535 fn open_host_stream(
537 &self,
538 dependency: PluginStreamDependencyHandle,
539 operation: String,
540 request: Value,
541 context: InvocationContext,
542 ) -> JsonHostStreamOpenFuture {
543 let _ = (dependency, request, context);
544 Box::pin(futures::future::ready(Err(unknown_operation(
545 self.capability_id(),
546 &operation,
547 ))))
548 }
549}
550
551#[derive(Debug)]
553pub enum JsonInvocationOutcome {
554 Success(Value),
556 DomainError(Value),
558}
559
560pub fn json_runtime_failure(error: &RuntimeFailure) -> Value {
562 match error {
563 RuntimeFailure::Unavailable { capability } => serde_json::json!({
564 "kind": "unavailable",
565 "capability": capability,
566 }),
567 RuntimeFailure::UnknownOperation {
568 capability,
569 operation,
570 } => serde_json::json!({
571 "kind": "unknown_operation",
572 "capability": capability,
573 "operation": operation,
574 }),
575 RuntimeFailure::AmbiguousBinding {
576 capability,
577 providers,
578 } => serde_json::json!({
579 "kind": "ambiguous_binding",
580 "capability": capability,
581 "providers": providers,
582 }),
583 RuntimeFailure::ProtocolViolation { capability } => serde_json::json!({
584 "kind": "protocol_violation",
585 "capability": capability,
586 }),
587 RuntimeFailure::AdmissionClosed => serde_json::json!({ "kind": "admission_closed" }),
588 RuntimeFailure::ResourceExhausted {
589 capability,
590 operation,
591 } => serde_json::json!({
592 "kind": "resource_exhausted",
593 "capability": capability,
594 "operation": operation,
595 }),
596 RuntimeFailure::DeadlineExceeded { request_id } => serde_json::json!({
597 "kind": "deadline_exceeded",
598 "request_id": request_id.to_string(),
599 }),
600 RuntimeFailure::Cancelled { request_id } => serde_json::json!({
601 "kind": "cancelled",
602 "request_id": request_id.to_string(),
603 }),
604 RuntimeFailure::MissingPluginFactory { .. }
605 | RuntimeFailure::UnavailableExecutionClass { .. }
606 | RuntimeFailure::InvalidResolvedPlan { .. }
607 | RuntimeFailure::Internal { .. }
608 | RuntimeFailure::PluginFailure { .. }
609 | RuntimeFailure::PluginRestartExhausted { .. } => {
610 serde_json::json!({ "kind": "internal" })
611 }
612 }
613}
614
615pub fn json_host_invocation_envelope(
617 outcome: Result<JsonInvocationOutcome, RuntimeFailure>,
618) -> Value {
619 match outcome {
620 Ok(JsonInvocationOutcome::Success(value)) => serde_json::json!({ "ok": value }),
621 Ok(JsonInvocationOutcome::DomainError(value)) => serde_json::json!({ "error": value }),
622 Err(error) => serde_json::json!({ "runtime": json_runtime_failure(&error) }),
623 }
624}
625
626pub type JsonHostRequestFuture =
628 futures::future::LocalBoxFuture<'static, Result<JsonInvocationOutcome, RuntimeFailure>>;
629
630pub trait JsonHostStreamSession: std::fmt::Debug + 'static {
632 fn send(
633 self: Rc<Self>,
634 message: Value,
635 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
636 fn receive(
637 self: Rc<Self>,
638 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>>;
639 fn close_send(
640 self: Rc<Self>,
641 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
642 fn cancel(&self);
643}
644
645pub type JsonHostStreamOpenFuture = futures::future::LocalBoxFuture<
647 'static,
648 Result<Result<Rc<dyn JsonHostStreamSession>, Value>, RuntimeFailure>,
649>;
650
651type DecodeStreamMessage<C> =
652 Rc<dyn Fn(Value) -> Result<<C as StreamCapability>::Message, RuntimeFailure>>;
653type EncodeStreamMessage<C> =
654 Rc<dyn Fn(<C as StreamCapability>::Message) -> Result<Value, RuntimeFailure>>;
655type EncodeStreamError<C> =
656 Rc<dyn Fn(<C as StreamCapability>::DomainError) -> Result<Value, RuntimeFailure>>;
657
658pub fn json_host_stream<C: StreamCapability>(
660 stream: NativeStream<C>,
661 decode_message: impl Fn(Value) -> Result<C::Message, RuntimeFailure> + 'static,
662 encode_message: impl Fn(C::Message) -> Result<Value, RuntimeFailure> + 'static,
663 encode_error: impl Fn(C::DomainError) -> Result<Value, RuntimeFailure> + 'static,
664) -> Rc<dyn JsonHostStreamSession> {
665 Rc::new(TypedJsonHostStream {
666 stream: Rc::new(stream),
667 decode_message: Rc::new(decode_message),
668 encode_message: Rc::new(encode_message),
669 encode_error: Rc::new(encode_error),
670 })
671}
672
673struct TypedJsonHostStream<C: StreamCapability> {
674 stream: Rc<NativeStream<C>>,
675 decode_message: DecodeStreamMessage<C>,
676 encode_message: EncodeStreamMessage<C>,
677 encode_error: EncodeStreamError<C>,
678}
679
680impl<C: StreamCapability> std::fmt::Debug for TypedJsonHostStream<C> {
681 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682 formatter
683 .debug_struct("TypedJsonHostStream")
684 .field("capability", &C::ID)
685 .finish_non_exhaustive()
686 }
687}
688
689impl<C: StreamCapability> JsonHostStreamSession for TypedJsonHostStream<C> {
690 fn send(
691 self: Rc<Self>,
692 message: Value,
693 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
694 Box::pin(async move {
695 let message = (self.decode_message)(message)?;
696 self.stream.send(message).await
697 })
698 }
699
700 fn receive(
701 self: Rc<Self>,
702 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>> {
703 Box::pin(async move {
704 match self.stream.receive().await? {
705 StreamEvent::Message(message) => {
706 (self.encode_message)(message).map(JsonStreamItem::Message)
707 }
708 StreamEvent::PeerHalfClosed => Ok(JsonStreamItem::PeerHalfClosed),
709 StreamEvent::Terminal(Ok(())) => Ok(JsonStreamItem::Terminal(Ok(()))),
710 StreamEvent::Terminal(Err(error)) => {
711 (self.encode_error)(error).map(|error| JsonStreamItem::Terminal(Err(error)))
712 }
713 }
714 })
715 }
716
717 fn close_send(
718 self: Rc<Self>,
719 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
720 Box::pin(async move { self.stream.close_send().await })
721 }
722
723 fn cancel(&self) {
724 self.stream.cancel();
725 }
726}
727
728pub const JSON_REQUEST_ABI_V1: &str = "lenso.json-request@1";
730
731pub const JSON_INTERACTIONS_ABI_V1: &str = "lenso.json-interactions@1";
733
734pub const JSON_HOST_IMPORTS_ABI_V1: &str = "lenso.json-host-imports@1";
736pub const JSON_HOST_IMPORTS_ABI_V2: &str = "lenso.json-host-imports@2";
738
739#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
741#[serde(deny_unknown_fields)]
742pub struct JsonPluginDescriptor {
743 pub abi: String,
744 pub capabilities: Vec<JsonCapabilityDescriptor>,
745 #[serde(default, skip_serializing_if = "Vec::is_empty")]
746 pub required_capabilities: Vec<JsonRequiredCapabilityDescriptor>,
747}
748
749#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
751#[serde(deny_unknown_fields)]
752pub struct JsonCapabilityDescriptor {
753 pub capability_id: String,
754 pub descriptor_version: String,
755 pub request_operations: Vec<String>,
756 #[serde(default, skip_serializing_if = "Vec::is_empty")]
757 pub stream_operations: Vec<String>,
758}
759
760#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
762#[serde(deny_unknown_fields)]
763pub struct JsonRequiredCapabilityDescriptor {
764 pub requirement_id: String,
765 pub capability_id: String,
766 pub descriptor_version: String,
767 pub cardinality: CapabilityCardinality,
768}
769
770pub fn expected_json_plugin_descriptor(
772 instance: &PluginInstancePlan,
773) -> Result<JsonPluginDescriptor, RuntimeFailure> {
774 let mut capabilities = Vec::with_capacity(instance.provided_capabilities().len());
775 for descriptor in instance.provided_capabilities() {
776 if !descriptor.event_operations().is_empty() {
777 return Err(RuntimeFailure::InvalidResolvedPlan {
778 detail: format!(
779 "Execution class `{}` does not support Event endpoints",
780 instance.execution_class()
781 ),
782 });
783 }
784 capabilities.push(JsonCapabilityDescriptor {
785 capability_id: descriptor.capability_id().to_owned(),
786 descriptor_version: descriptor.descriptor_version().to_owned(),
787 request_operations: descriptor
788 .request_operations()
789 .into_iter()
790 .map(str::to_owned)
791 .collect(),
792 stream_operations: descriptor
793 .stream_operations()
794 .into_iter()
795 .map(str::to_owned)
796 .collect(),
797 });
798 }
799 capabilities.sort();
800 if capabilities
801 .windows(2)
802 .any(|pair| pair[0].capability_id == pair[1].capability_id)
803 {
804 return Err(RuntimeFailure::InvalidResolvedPlan {
805 detail: format!(
806 "Instance `{}` declares a duplicate Capability",
807 instance.instance_key()
808 ),
809 });
810 }
811 let mut required_capabilities = instance
812 .required_capabilities()
813 .iter()
814 .map(|requirement| JsonRequiredCapabilityDescriptor {
815 requirement_id: requirement.requirement_id().to_owned(),
816 capability_id: requirement.capability_id().to_owned(),
817 descriptor_version: requirement.descriptor_version().to_owned(),
818 cardinality: requirement.cardinality(),
819 })
820 .collect::<Vec<_>>();
821 sort_required_capabilities(&mut required_capabilities);
822 Ok(JsonPluginDescriptor {
823 abi: if !required_capabilities.is_empty() {
824 JSON_HOST_IMPORTS_ABI_V2
825 } else if capabilities
826 .iter()
827 .any(|capability| !capability.stream_operations.is_empty())
828 {
829 JSON_INTERACTIONS_ABI_V1
830 } else {
831 JSON_REQUEST_ABI_V1
832 }
833 .to_owned(),
834 capabilities,
835 required_capabilities,
836 })
837}
838
839pub fn validate_json_plugin_descriptor(
841 instance: &PluginInstancePlan,
842 encoded: &str,
843) -> Result<(), RuntimeFailure> {
844 let mut actual = serde_json::from_str::<JsonPluginDescriptor>(encoded).map_err(|_| {
845 RuntimeFailure::ProtocolViolation {
846 capability: "lenso.json-request@1",
847 }
848 })?;
849 actual.capabilities.sort();
850 sort_required_capabilities(&mut actual.required_capabilities);
851 let expected = expected_json_plugin_descriptor(instance)?;
852 if actual != expected {
853 return Err(RuntimeFailure::InvalidResolvedPlan {
854 detail: format!(
855 "guest descriptor does not match resolved Instance `{}`",
856 instance.instance_key()
857 ),
858 });
859 }
860 Ok(())
861}
862
863fn sort_required_capabilities(requirements: &mut [JsonRequiredCapabilityDescriptor]) {
864 requirements.sort_by(|left, right| {
865 (
866 &left.requirement_id,
867 &left.capability_id,
868 &left.descriptor_version,
869 cardinality_order(left.cardinality),
870 )
871 .cmp(&(
872 &right.requirement_id,
873 &right.capability_id,
874 &right.descriptor_version,
875 cardinality_order(right.cardinality),
876 ))
877 });
878}
879
880const fn cardinality_order(cardinality: CapabilityCardinality) -> u8 {
881 match cardinality {
882 CapabilityCardinality::One => 0,
883 CapabilityCardinality::Optional => 1,
884 CapabilityCardinality::Many => 2,
885 }
886}
887
888pub trait JsonRequestTransport: std::fmt::Debug + 'static {
890 fn invoke(
891 self: Rc<Self>,
892 capability: String,
893 operation: String,
894 request_json: String,
895 context: InvocationContext,
896 ) -> futures::future::LocalBoxFuture<'static, Result<JsonInvocationOutcome, RuntimeFailure>>;
897}
898
899#[derive(Debug)]
901pub enum JsonStreamItem {
902 Message(Value),
903 PeerHalfClosed,
904 Terminal(Result<(), Value>),
905}
906
907#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
909#[serde(
910 tag = "kind",
911 content = "value",
912 rename_all = "kebab-case",
913 deny_unknown_fields
914)]
915pub enum JsonStreamFrame {
916 Message(Value),
917 PeerHalfClosed,
918 TerminalSuccess,
919 TerminalError(Value),
920}
921
922#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
924pub struct JsonHostBindingDescriptor {
925 pub binding_id: u32,
926 pub requirement_id: String,
927 pub provider_instance: String,
928 pub capability_id: String,
929 pub descriptor_version: String,
930 #[serde(skip_serializing)]
931 pub descriptor_digest: String,
932 pub request_operations: Vec<String>,
933 pub stream_operations: Vec<String>,
934}
935
936#[derive(Clone)]
937struct JsonHostBinding {
938 descriptor: JsonHostBindingDescriptor,
939 codec: Rc<dyn JsonCapabilityCodec>,
940 request: Option<PluginDependencyHandle>,
941 stream: Option<PluginStreamDependencyHandle>,
942}
943
944impl std::fmt::Debug for JsonHostBinding {
945 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
946 formatter
947 .debug_struct("JsonHostBinding")
948 .field("descriptor", &self.descriptor)
949 .finish_non_exhaustive()
950 }
951}
952
953#[derive(Debug)]
955pub struct JsonHostImports {
956 codecs: BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
957 bindings: std::cell::RefCell<Option<Vec<JsonHostBinding>>>,
958 streams: std::cell::RefCell<BTreeMap<u64, Rc<dyn JsonHostStreamSession>>>,
959 next_stream_id: std::cell::Cell<u64>,
960 max_streams: usize,
961}
962
963impl JsonHostImports {
964 pub fn new(
966 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
967 max_streams: usize,
968 ) -> Result<Self, RuntimeFailure> {
969 let mut by_capability = BTreeMap::new();
970 for codec in codecs {
971 let capability = codec.capability_id().to_owned();
972 if let Some(existing) = by_capability.get(&capability) {
973 if !Rc::ptr_eq(existing, &codec) {
974 return Err(RuntimeFailure::InvalidResolvedPlan {
975 detail: format!(
976 "conflicting guest import codecs for Capability `{capability}`"
977 ),
978 });
979 }
980 } else {
981 by_capability.insert(capability, codec);
982 }
983 }
984 Ok(Self {
985 codecs: by_capability,
986 bindings: std::cell::RefCell::new(None),
987 streams: std::cell::RefCell::new(BTreeMap::new()),
988 next_stream_id: std::cell::Cell::new(1),
989 max_streams,
990 })
991 }
992
993 pub fn activate(&self, dependencies: &PluginDependencies) -> Result<(), RuntimeFailure> {
995 if self.bindings.borrow().is_some() {
996 return Err(RuntimeFailure::Internal {
997 detail: "guest Capability imports were activated twice".to_owned(),
998 });
999 }
1000 let mut bindings = Vec::with_capacity(dependencies.len());
1001 for (index, dependency) in dependencies.bindings().iter().enumerate() {
1002 let codec = self
1003 .codecs
1004 .get(dependency.capability_id())
1005 .cloned()
1006 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
1007 detail: format!(
1008 "no generated guest import codec for Capability `{}`",
1009 dependency.capability_id()
1010 ),
1011 })?;
1012 let request = dependency.handle();
1013 let stream = dependency.stream_handle();
1014 validate_host_binding(&codec, request.as_ref(), stream.as_ref())?;
1015 let binding_id =
1016 u32::try_from(index).map_err(|_| RuntimeFailure::InvalidResolvedPlan {
1017 detail: "guest import binding table exceeds u32 identity space".to_owned(),
1018 })?;
1019 bindings.push(JsonHostBinding {
1020 descriptor: JsonHostBindingDescriptor {
1021 binding_id,
1022 requirement_id: dependency.requirement_id().to_owned(),
1023 provider_instance: dependency.provider_instance().to_owned(),
1024 capability_id: dependency.capability_id().to_owned(),
1025 descriptor_version: codec.descriptor_version().to_owned(),
1026 descriptor_digest: codec.descriptor_digest().to_owned(),
1027 request_operations: request.as_ref().map_or_else(Vec::new, |handle| {
1028 handle
1029 .operations()
1030 .iter()
1031 .map(|item| (*item).to_owned())
1032 .collect()
1033 }),
1034 stream_operations: stream.as_ref().map_or_else(Vec::new, |handle| {
1035 handle
1036 .operations()
1037 .iter()
1038 .map(|item| (*item).to_owned())
1039 .collect()
1040 }),
1041 },
1042 codec,
1043 request,
1044 stream,
1045 });
1046 }
1047 self.bindings.replace(Some(bindings));
1048 Ok(())
1049 }
1050
1051 pub fn descriptors(&self) -> Result<Vec<JsonHostBindingDescriptor>, RuntimeFailure> {
1053 self.bindings
1054 .borrow()
1055 .as_ref()
1056 .map(|bindings| {
1057 bindings
1058 .iter()
1059 .map(|binding| binding.descriptor.clone())
1060 .collect()
1061 })
1062 .ok_or(RuntimeFailure::AdmissionClosed)
1063 }
1064
1065 pub fn invoke(
1067 &self,
1068 binding_id: u32,
1069 operation: String,
1070 request: Value,
1071 context: InvocationContext,
1072 ) -> JsonHostRequestFuture {
1073 let binding = match self.binding(binding_id) {
1074 Ok(binding) => binding,
1075 Err(error) => return Box::pin(futures::future::ready(Err(error))),
1076 };
1077 let Some(dependency) = binding.request else {
1078 return Box::pin(futures::future::ready(Err(
1079 RuntimeFailure::UnknownOperation {
1080 capability: binding.codec.capability_id(),
1081 operation,
1082 },
1083 )));
1084 };
1085 binding
1086 .codec
1087 .invoke_host_request(dependency, operation, request, context)
1088 }
1089
1090 pub fn open_stream(
1092 self: Rc<Self>,
1093 binding_id: u32,
1094 operation: String,
1095 request: Value,
1096 context: InvocationContext,
1097 ) -> futures::future::LocalBoxFuture<'static, Result<Result<u64, Value>, RuntimeFailure>> {
1098 Box::pin(async move {
1099 if self.streams.borrow().len() >= self.max_streams {
1100 return Err(RuntimeFailure::ResourceExhausted {
1101 capability: JSON_HOST_IMPORTS_ABI_V2,
1102 operation: "stream-open".to_owned(),
1103 });
1104 }
1105 let binding = self.binding(binding_id)?;
1106 let dependency = binding
1107 .stream
1108 .ok_or_else(|| RuntimeFailure::UnknownOperation {
1109 capability: binding.codec.capability_id(),
1110 operation: operation.clone(),
1111 })?;
1112 match binding
1113 .codec
1114 .open_host_stream(dependency, operation, request, context)
1115 .await?
1116 {
1117 Ok(stream) => {
1118 let stream_id = self.next_stream_id.get();
1119 let next =
1120 stream_id
1121 .checked_add(1)
1122 .ok_or(RuntimeFailure::ResourceExhausted {
1123 capability: JSON_HOST_IMPORTS_ABI_V2,
1124 operation: "stream-open".to_owned(),
1125 })?;
1126 self.next_stream_id.set(next);
1127 self.streams.borrow_mut().insert(stream_id, stream);
1128 Ok(Ok(stream_id))
1129 }
1130 Err(error) => Ok(Err(error)),
1131 }
1132 })
1133 }
1134
1135 pub fn send_stream(
1137 &self,
1138 stream_id: u64,
1139 message: Value,
1140 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1141 match self.stream(stream_id) {
1142 Ok(stream) => stream.send(message),
1143 Err(error) => Box::pin(futures::future::ready(Err(error))),
1144 }
1145 }
1146
1147 pub fn receive_stream(
1149 self: Rc<Self>,
1150 stream_id: u64,
1151 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>> {
1152 Box::pin(async move {
1153 let stream = self.stream(stream_id)?;
1154 let item = stream.receive().await?;
1155 if matches!(item, JsonStreamItem::Terminal(_)) {
1156 self.streams.borrow_mut().remove(&stream_id);
1157 }
1158 Ok(item)
1159 })
1160 }
1161
1162 pub fn close_stream_send(
1164 &self,
1165 stream_id: u64,
1166 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1167 match self.stream(stream_id) {
1168 Ok(stream) => stream.close_send(),
1169 Err(error) => Box::pin(futures::future::ready(Err(error))),
1170 }
1171 }
1172
1173 pub fn cancel_stream(&self, stream_id: u64) -> Result<(), RuntimeFailure> {
1175 let stream = self
1176 .streams
1177 .borrow_mut()
1178 .remove(&stream_id)
1179 .ok_or_else(unknown_host_stream)?;
1180 stream.cancel();
1181 Ok(())
1182 }
1183
1184 pub fn deactivate(&self) {
1186 self.bindings.replace(None);
1187 for (_, stream) in std::mem::take(&mut *self.streams.borrow_mut()) {
1188 stream.cancel();
1189 }
1190 }
1191
1192 fn binding(&self, binding_id: u32) -> Result<JsonHostBinding, RuntimeFailure> {
1193 let bindings = self.bindings.borrow();
1194 let bindings = bindings.as_ref().ok_or(RuntimeFailure::AdmissionClosed)?;
1195 bindings
1196 .get(binding_id as usize)
1197 .cloned()
1198 .ok_or(RuntimeFailure::ProtocolViolation {
1199 capability: JSON_HOST_IMPORTS_ABI_V2,
1200 })
1201 }
1202
1203 fn stream(&self, stream_id: u64) -> Result<Rc<dyn JsonHostStreamSession>, RuntimeFailure> {
1204 self.streams
1205 .borrow()
1206 .get(&stream_id)
1207 .cloned()
1208 .ok_or_else(unknown_host_stream)
1209 }
1210}
1211
1212fn validate_host_binding(
1213 codec: &Rc<dyn JsonCapabilityCodec>,
1214 request: Option<&PluginDependencyHandle>,
1215 stream: Option<&PluginStreamDependencyHandle>,
1216) -> Result<(), RuntimeFailure> {
1217 for (capability, version) in request
1218 .map(|handle| (handle.capability_id(), handle.descriptor_version()))
1219 .into_iter()
1220 .chain(stream.map(|handle| (handle.capability_id(), handle.descriptor_version())))
1221 {
1222 if capability != codec.capability_id() || version != codec.descriptor_version() {
1223 return Err(RuntimeFailure::ProtocolViolation {
1224 capability: codec.capability_id(),
1225 });
1226 }
1227 }
1228 Ok(())
1229}
1230
1231fn unknown_host_stream() -> RuntimeFailure {
1232 RuntimeFailure::ProtocolViolation {
1233 capability: JSON_HOST_IMPORTS_ABI_V2,
1234 }
1235}
1236
1237impl JsonStreamFrame {
1238 pub fn decode(
1240 encoded: &str,
1241 capability: &'static str,
1242 ) -> Result<JsonStreamItem, RuntimeFailure> {
1243 match serde_json::from_str(encoded)
1244 .map_err(|_| RuntimeFailure::ProtocolViolation { capability })?
1245 {
1246 Self::Message(value) => Ok(JsonStreamItem::Message(value)),
1247 Self::PeerHalfClosed => Ok(JsonStreamItem::PeerHalfClosed),
1248 Self::TerminalSuccess => Ok(JsonStreamItem::Terminal(Ok(()))),
1249 Self::TerminalError(value) => Ok(JsonStreamItem::Terminal(Err(value))),
1250 }
1251 }
1252}
1253
1254pub trait JsonStreamSessionTransport: std::fmt::Debug + 'static {
1256 fn send(
1257 self: Rc<Self>,
1258 message_json: String,
1259 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
1260 fn receive(
1261 self: Rc<Self>,
1262 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>>;
1263 fn close_send(
1264 self: Rc<Self>,
1265 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
1266 fn cancel(&self);
1267}
1268
1269pub type JsonStreamOpenFuture = futures::future::LocalBoxFuture<
1271 'static,
1272 Result<Result<Rc<dyn JsonStreamSessionTransport>, Value>, RuntimeFailure>,
1273>;
1274
1275pub trait JsonStreamTransport: std::fmt::Debug + 'static {
1277 fn open(
1278 self: Rc<Self>,
1279 capability: String,
1280 operation: String,
1281 request_json: String,
1282 context: InvocationContext,
1283 ) -> JsonStreamOpenFuture;
1284}
1285
1286pub fn json_request_endpoints<T: JsonRequestTransport>(
1288 transport: Rc<T>,
1289 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
1290) -> Vec<Rc<dyn NativeRequestEndpoint>> {
1291 let transport: Rc<dyn JsonRequestTransport> = transport;
1292 codecs
1293 .into_iter()
1294 .filter(|codec| !codec.request_operations().is_empty())
1295 .map(|codec| {
1296 Rc::new(JsonRequestEndpoint {
1297 transport: transport.clone(),
1298 codec,
1299 }) as Rc<dyn NativeRequestEndpoint>
1300 })
1301 .collect()
1302}
1303
1304pub fn json_stream_endpoints<T: JsonStreamTransport>(
1306 transport: Rc<T>,
1307 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
1308) -> Vec<Rc<dyn NativeStreamEndpoint>> {
1309 let transport: Rc<dyn JsonStreamTransport> = transport;
1310 codecs
1311 .into_iter()
1312 .filter(|codec| !codec.stream_operations().is_empty())
1313 .map(|codec| {
1314 Rc::new(JsonStreamEndpoint {
1315 transport: transport.clone(),
1316 codec,
1317 }) as Rc<dyn NativeStreamEndpoint>
1318 })
1319 .collect()
1320}
1321
1322#[derive(Debug)]
1323struct JsonStreamEndpoint {
1324 transport: Rc<dyn JsonStreamTransport>,
1325 codec: Rc<dyn JsonCapabilityCodec>,
1326}
1327
1328impl NativeStreamEndpoint for JsonStreamEndpoint {
1329 fn capability_id(&self) -> &'static str {
1330 self.codec.capability_id()
1331 }
1332 fn descriptor_version(&self) -> &'static str {
1333 self.codec.descriptor_version()
1334 }
1335 fn operations(&self) -> &'static [&'static str] {
1336 self.codec.stream_operations()
1337 }
1338
1339 fn open(
1340 &self,
1341 operation: &str,
1342 request: Box<dyn Any>,
1343 context: InvocationContext,
1344 ) -> futures::future::LocalBoxFuture<
1345 'static,
1346 Result<Result<Box<dyn NativeStreamSession>, Box<dyn Any>>, RuntimeFailure>,
1347 > {
1348 let transport = self.transport.clone();
1349 let codec = self.codec.clone();
1350 let operation = operation.to_owned();
1351 Box::pin(async move {
1352 if !codec.stream_operations().contains(&operation.as_str()) {
1353 return Err(unknown_operation(codec.capability_id(), &operation));
1354 }
1355 let request = codec.encode_stream_open(&operation, request.as_ref())?;
1356 let request_json =
1357 serde_json::to_string(&request).map_err(|_| RuntimeFailure::ProtocolViolation {
1358 capability: codec.capability_id(),
1359 })?;
1360 match transport
1361 .open(
1362 codec.capability_id().to_owned(),
1363 operation.clone(),
1364 request_json,
1365 context,
1366 )
1367 .await?
1368 {
1369 Ok(session) => Ok(Ok(Box::new(JsonStreamSession {
1370 session,
1371 codec,
1372 operation,
1373 }) as Box<dyn NativeStreamSession>)),
1374 Err(error) => codec.decode_stream_domain_error(&operation, error).map(Err),
1375 }
1376 })
1377 }
1378}
1379
1380#[derive(Debug)]
1381struct JsonStreamSession {
1382 session: Rc<dyn JsonStreamSessionTransport>,
1383 codec: Rc<dyn JsonCapabilityCodec>,
1384 operation: String,
1385}
1386
1387impl NativeStreamSession for JsonStreamSession {
1388 fn send(
1389 &self,
1390 message: Box<dyn Any>,
1391 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1392 let encoded = self
1393 .codec
1394 .encode_stream_message(&self.operation, message.as_ref())
1395 .and_then(|value| {
1396 serde_json::to_string(&value).map_err(|_| RuntimeFailure::ProtocolViolation {
1397 capability: self.codec.capability_id(),
1398 })
1399 });
1400 let session = self.session.clone();
1401 Box::pin(async move { session.send(encoded?).await })
1402 }
1403
1404 fn receive(
1405 &self,
1406 ) -> futures::future::LocalBoxFuture<'static, Result<NativeStreamItem, RuntimeFailure>> {
1407 let session = self.session.clone();
1408 let codec = self.codec.clone();
1409 let operation = self.operation.clone();
1410 Box::pin(async move {
1411 match session.receive().await? {
1412 JsonStreamItem::Message(value) => codec
1413 .decode_stream_message(&operation, value)
1414 .map(NativeStreamItem::Message),
1415 JsonStreamItem::PeerHalfClosed => Ok(NativeStreamItem::PeerHalfClosed),
1416 JsonStreamItem::Terminal(Ok(())) => Ok(NativeStreamItem::Terminal(Ok(()))),
1417 JsonStreamItem::Terminal(Err(value)) => codec
1418 .decode_stream_domain_error(&operation, value)
1419 .map(|error| NativeStreamItem::Terminal(Err(error))),
1420 }
1421 })
1422 }
1423
1424 fn close_send(&self) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1425 self.session.clone().close_send()
1426 }
1427
1428 fn cancel(&self) {
1429 self.session.cancel();
1430 }
1431}
1432
1433#[derive(Debug)]
1434struct JsonRequestEndpoint {
1435 transport: Rc<dyn JsonRequestTransport>,
1436 codec: Rc<dyn JsonCapabilityCodec>,
1437}
1438
1439impl NativeRequestEndpoint for JsonRequestEndpoint {
1440 fn capability_id(&self) -> &'static str {
1441 self.codec.capability_id()
1442 }
1443
1444 fn descriptor_version(&self) -> &'static str {
1445 self.codec.descriptor_version()
1446 }
1447
1448 fn operations(&self) -> &'static [&'static str] {
1449 self.codec.request_operations()
1450 }
1451
1452 fn invoke(
1453 &self,
1454 operation: &str,
1455 request: Box<dyn Any>,
1456 context: InvocationContext,
1457 ) -> futures::future::LocalBoxFuture<
1458 'static,
1459 Result<Result<Box<dyn Any>, Box<dyn Any>>, RuntimeFailure>,
1460 > {
1461 let transport = self.transport.clone();
1462 let codec = self.codec.clone();
1463 let operation = operation.to_owned();
1464 Box::pin(async move {
1465 if !codec.request_operations().contains(&operation.as_str()) {
1466 return Err(RuntimeFailure::UnknownOperation {
1467 capability: codec.capability_id(),
1468 operation,
1469 });
1470 }
1471 let request = codec.encode_request(&operation, request.as_ref())?;
1472 let request =
1473 serde_json::to_string(&request).map_err(|_| RuntimeFailure::ProtocolViolation {
1474 capability: codec.capability_id(),
1475 })?;
1476 match transport
1477 .invoke(
1478 codec.capability_id().to_owned(),
1479 operation.clone(),
1480 request,
1481 context,
1482 )
1483 .await?
1484 {
1485 JsonInvocationOutcome::Success(value) => {
1486 codec.decode_response(&operation, value).map(Ok)
1487 }
1488 JsonInvocationOutcome::DomainError(value) => {
1489 codec.decode_domain_error(&operation, value).map(Err)
1490 }
1491 }
1492 })
1493 }
1494}
1495
1496pub fn codecs_for_instance(
1498 instance: &PluginInstancePlan,
1499 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1500) -> Result<Vec<Rc<dyn JsonCapabilityCodec>>, RuntimeFailure> {
1501 let mut selected = Vec::with_capacity(instance.provided_capabilities().len());
1502 for descriptor in instance.provided_capabilities() {
1503 if !descriptor.event_operations().is_empty() {
1504 return Err(RuntimeFailure::InvalidResolvedPlan {
1505 detail: format!(
1506 "Execution class `{}` does not support Event endpoints",
1507 instance.execution_class()
1508 ),
1509 });
1510 }
1511 let codec = codecs.get(descriptor.capability_id()).ok_or_else(|| {
1512 RuntimeFailure::InvalidResolvedPlan {
1513 detail: format!(
1514 "no generated codec for Capability `{}`",
1515 descriptor.capability_id()
1516 ),
1517 }
1518 })?;
1519 let request_operations: Vec<_> = codec
1520 .request_operations()
1521 .iter()
1522 .map(|operation| (*operation).to_owned())
1523 .collect();
1524 let stream_operations: Vec<_> = codec
1525 .stream_operations()
1526 .iter()
1527 .map(|operation| (*operation).to_owned())
1528 .collect();
1529 let expected_request: Vec<_> = descriptor
1530 .request_operations()
1531 .into_iter()
1532 .map(str::to_owned)
1533 .collect();
1534 let expected_stream: Vec<_> = descriptor
1535 .stream_operations()
1536 .into_iter()
1537 .map(str::to_owned)
1538 .collect();
1539 if codec.descriptor_version() != descriptor.descriptor_version()
1540 || request_operations != expected_request
1541 || stream_operations != expected_stream
1542 {
1543 return Err(RuntimeFailure::ProtocolViolation {
1544 capability: codec.capability_id(),
1545 });
1546 }
1547 selected.push(codec.clone());
1548 }
1549 Ok(selected)
1550}
1551
1552pub fn codecs_for_requirements(
1554 instance: &PluginInstancePlan,
1555 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1556) -> Result<Vec<Rc<dyn JsonCapabilityCodec>>, RuntimeFailure> {
1557 let mut selected = BTreeMap::new();
1558 for requirement in instance.required_capabilities() {
1559 let codec = codecs.get(requirement.capability_id()).ok_or_else(|| {
1560 RuntimeFailure::InvalidResolvedPlan {
1561 detail: format!(
1562 "no generated guest import codec for Capability `{}`",
1563 requirement.capability_id()
1564 ),
1565 }
1566 })?;
1567 if codec.descriptor_version() != requirement.descriptor_version() {
1568 return Err(RuntimeFailure::ProtocolViolation {
1569 capability: codec.capability_id(),
1570 });
1571 }
1572 selected
1573 .entry(requirement.capability_id().to_owned())
1574 .or_insert_with(|| codec.clone());
1575 }
1576 Ok(selected.into_values().collect())
1577}
1578
1579pub fn prepare_request_app(
1581 plan: &ResolvedAppPlan,
1582 execution_class: &ExecutionClassId,
1583 generations: BTreeMap<String, PreparedNativePlugin>,
1584) -> Result<PreparedNativeApp, RuntimeFailure> {
1585 let selected_instances = plan
1586 .plugin_instances()
1587 .iter()
1588 .filter(|instance| instance.execution_class() == execution_class)
1589 .map(|instance| instance.instance_key().to_owned())
1590 .collect::<std::collections::BTreeSet<_>>();
1591 let mut endpoints = BTreeMap::new();
1592 let mut stream_endpoints = BTreeMap::new();
1593 for (instance_key, generation) in &generations {
1594 for endpoint in generation.endpoints() {
1595 let identity = (instance_key.clone(), endpoint.capability_id().to_owned());
1596 if endpoints.insert(identity, endpoint.clone()).is_some() {
1597 return Err(RuntimeFailure::InvalidResolvedPlan {
1598 detail: format!("duplicate request endpoint on Instance `{instance_key}`"),
1599 });
1600 }
1601 }
1602 for endpoint in generation.stream_endpoints() {
1603 let identity = (instance_key.clone(), endpoint.capability_id().to_owned());
1604 if stream_endpoints
1605 .insert(identity, endpoint.clone())
1606 .is_some()
1607 {
1608 return Err(RuntimeFailure::InvalidResolvedPlan {
1609 detail: format!("duplicate stream endpoint on Instance `{instance_key}`"),
1610 });
1611 }
1612 }
1613 }
1614 for instance in plan
1615 .plugin_instances()
1616 .iter()
1617 .filter(|instance| selected_instances.contains(instance.instance_key()))
1618 {
1619 if !generations.contains_key(instance.instance_key()) {
1620 return Err(RuntimeFailure::InvalidResolvedPlan {
1621 detail: format!("Adapter omitted Instance `{}`", instance.instance_key()),
1622 });
1623 }
1624 }
1625 let mut bindings = Vec::new();
1626 let mut stream_bindings = Vec::new();
1627 for binding in plan.capability_bindings() {
1628 let key = (
1629 binding.provider_instance().to_owned(),
1630 binding.capability_id().to_owned(),
1631 );
1632 let request_endpoint = endpoints.get(&key);
1633 let stream_endpoint = stream_endpoints.get(&key);
1634 if let Some(endpoint) = request_endpoint {
1635 bindings.push(
1636 PreparedBinding::new(
1637 binding.consumer_instance(),
1638 binding.provider_instance(),
1639 endpoint.clone(),
1640 )
1641 .with_requirement_id(binding.requirement_id()),
1642 );
1643 }
1644 if let Some(endpoint) = stream_endpoint {
1645 stream_bindings.push(
1646 PreparedStreamBinding::new(
1647 binding.consumer_instance(),
1648 binding.provider_instance(),
1649 endpoint.clone(),
1650 )
1651 .with_requirement_id(binding.requirement_id()),
1652 );
1653 }
1654 if request_endpoint.is_none()
1655 && stream_endpoint.is_none()
1656 && selected_instances.contains(binding.provider_instance())
1657 {
1658 return Err(RuntimeFailure::InvalidResolvedPlan {
1659 detail: format!(
1660 "Adapter omitted Capability `{}` endpoint for Instance `{}`",
1661 binding.capability_id(),
1662 binding.provider_instance()
1663 ),
1664 });
1665 }
1666 }
1667 Ok(PreparedNativeApp::new(bindings, generations).with_stream_bindings(stream_bindings))
1668}
1669
1670pub fn require_operation(
1672 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1673 capability_id: &str,
1674 operation: &str,
1675) -> Result<Rc<dyn JsonCapabilityCodec>, RuntimeFailure> {
1676 let codec =
1677 codecs
1678 .get(capability_id)
1679 .cloned()
1680 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
1681 detail: format!("no generated codec for Capability `{capability_id}`"),
1682 })?;
1683 if !codec.request_operations().contains(&operation) {
1684 return Err(RuntimeFailure::UnknownOperation {
1685 capability: codec.capability_id(),
1686 operation: operation.to_owned(),
1687 });
1688 }
1689 Ok(codec)
1690}
1691
1692fn unknown_operation(capability: &'static str, operation: &str) -> RuntimeFailure {
1693 RuntimeFailure::UnknownOperation {
1694 capability,
1695 operation: operation.to_owned(),
1696 }
1697}
1698
1699fn validate_digest(digest: &str) -> Result<(), RuntimeFailure> {
1700 let valid = digest.strip_prefix("sha256:").is_some_and(|hex| {
1701 hex.len() == 64
1702 && hex
1703 .bytes()
1704 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
1705 });
1706 if valid {
1707 Ok(())
1708 } else {
1709 Err(RuntimeFailure::InvalidResolvedPlan {
1710 detail: format!("invalid canonical SHA-256 digest `{digest}`"),
1711 })
1712 }
1713}
1714
1715fn invalid_artifact(path: &Path, error: impl std::fmt::Display) -> RuntimeFailure {
1716 RuntimeFailure::InvalidResolvedPlan {
1717 detail: format!("cannot read Artifact `{}`: {error}", path.display()),
1718 }
1719}
1720
1721fn validate_resource_path(path: &str) -> Result<(), RuntimeFailure> {
1722 if path.is_empty()
1723 || path.starts_with('/')
1724 || path.contains(['\\', '\0'])
1725 || path
1726 .split('/')
1727 .any(|segment| segment.is_empty() || matches!(segment, "." | ".."))
1728 {
1729 return Err(invalid_resources(format!(
1730 "invalid Plugin resource path `{path}`"
1731 )));
1732 }
1733 Ok(())
1734}
1735
1736fn invalid_resources(detail: impl Into<String>) -> RuntimeFailure {
1737 RuntimeFailure::InvalidResolvedPlan {
1738 detail: detail.into(),
1739 }
1740}
1741
1742#[cfg(test)]
1743mod tests {
1744 use std::io::Write;
1745
1746 use super::*;
1747
1748 #[test]
1749 fn artifact_handle_keeps_the_admitted_bytes_after_source_drift() {
1750 let mut file = tempfile::NamedTempFile::new().unwrap();
1751 file.write_all(b"first").unwrap();
1752 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"first")));
1753 let handle = ArtifactHandle::open(file.path(), &digest, 5).unwrap();
1754 file.as_file_mut().set_len(0).unwrap();
1755 file.write_all(b"other").unwrap();
1756 assert_eq!(handle.read_verified().unwrap(), b"first");
1757 assert_ne!(handle.path(), file.path());
1758 assert_eq!(fs::read(handle.path()).unwrap(), b"first");
1759 }
1760
1761 #[test]
1762 fn artifact_snapshot_survives_source_parent_rename_and_replacement() {
1763 let workspace = tempfile::tempdir().unwrap();
1764 let selected = workspace.path().join("selected");
1765 fs::create_dir(&selected).unwrap();
1766 let source = selected.join("plugin");
1767 fs::write(&source, b"admitted").unwrap();
1768 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"admitted")));
1769
1770 let handle = ArtifactHandle::open(&source, &digest, 8).unwrap();
1771 assert!(!handle.path().starts_with(&selected));
1772 fs::rename(&selected, workspace.path().join("replaced")).unwrap();
1773 fs::create_dir(&selected).unwrap();
1774 fs::write(selected.join("plugin"), b"attacker").unwrap();
1775
1776 assert_eq!(fs::read(handle.path()).unwrap(), b"admitted");
1777 assert_eq!(handle.read_verified().unwrap(), b"admitted");
1778 }
1779
1780 #[test]
1781 fn artifact_admission_streams_large_content_into_one_stable_snapshot() {
1782 let bytes = vec![0x5a; 4 * 1024 * 1024 + 17];
1783 let mut file = tempfile::NamedTempFile::new().unwrap();
1784 file.write_all(&bytes).unwrap();
1785 let digest = format!("sha256:{}", hex::encode(Sha256::digest(&bytes)));
1786
1787 let handle = ArtifactHandle::open(file.path(), &digest, bytes.len() as u64).unwrap();
1788
1789 assert_eq!(handle.read_verified().unwrap(), bytes);
1790 }
1791
1792 #[test]
1795 #[ignore = "large Artifact admission benchmark; run explicitly"]
1796 fn artifact_admission_streaming_benchmark() {
1797 const BLOCK_BYTES: usize = 64 * 1024;
1798 let block = vec![0x5a; BLOCK_BYTES];
1799 for mebibytes in [4_usize, 64, 256] {
1800 let directory = tempfile::tempdir().unwrap();
1801 let path = directory.path().join("artifact");
1802 let mut source = fs::File::create(&path).unwrap();
1803 let mut hasher = Sha256::new();
1804 let blocks = mebibytes * 1024 * 1024 / BLOCK_BYTES;
1805 for _ in 0..blocks {
1806 source.write_all(&block).unwrap();
1807 hasher.update(&block);
1808 }
1809 drop(source);
1810 let size = u64::try_from(mebibytes * 1024 * 1024).unwrap();
1811 let digest = format!("sha256:{}", hex::encode(hasher.finalize()));
1812
1813 let started = std::time::Instant::now();
1814 let handle = ArtifactHandle::open(&path, &digest, size).unwrap();
1815 let elapsed = started.elapsed();
1816
1817 assert_eq!(handle.size(), size);
1818 println!(
1819 "{{\"mebibytes\":{mebibytes},\"elapsed_ms\":{:.3},\"mib_per_second\":{:.3}}}",
1820 elapsed.as_secs_f64() * 1_000.0,
1821 f64::from(u32::try_from(mebibytes).unwrap()) / elapsed.as_secs_f64()
1822 );
1823 drop(handle);
1824 }
1825 }
1826
1827 #[test]
1828 fn artifact_admission_honors_an_explicit_host_staging_root() {
1829 let source = tempfile::tempdir().unwrap();
1830 let staging = tempfile::tempdir().unwrap();
1831 let path = source.path().join("plugin");
1832 fs::write(&path, b"artifact").unwrap();
1833 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"artifact")));
1834
1835 let handle =
1836 ArtifactHandle::open_with_staging_root(&path, &digest, 8, staging.path()).unwrap();
1837
1838 assert_eq!(
1839 handle.path().parent().unwrap().parent().unwrap(),
1840 staging.path()
1841 );
1842 }
1843
1844 #[test]
1845 fn instance_resources_are_order_independent_and_immutable() {
1846 let left = InstanceResources::from_files([
1847 ("prompts/system.md".to_owned(), b"Build carefully.".to_vec()),
1848 ("rules.toml".to_owned(), b"turns = 4\n".to_vec()),
1849 ])
1850 .unwrap();
1851 let right = InstanceResources::from_files([
1852 ("rules.toml".to_owned(), b"turns = 4\n".to_vec()),
1853 ("prompts/system.md".to_owned(), b"Build carefully.".to_vec()),
1854 ])
1855 .unwrap();
1856
1857 assert_eq!(left.digest(), right.digest());
1858 assert_eq!(
1859 left.read_text("prompts/system.md").unwrap(),
1860 "Build carefully."
1861 );
1862 assert_eq!(left.file_count(), 2);
1863 assert_eq!(left.total_size(), 26);
1864 }
1865
1866 #[test]
1867 fn instance_resources_reject_escaping_and_duplicate_paths() {
1868 assert!(InstanceResources::from_files([("../secret".to_owned(), Vec::new())]).is_err());
1869 assert!(
1870 InstanceResources::from_files([
1871 ("rules.toml".to_owned(), Vec::new()),
1872 ("rules.toml".to_owned(), Vec::new()),
1873 ])
1874 .is_err()
1875 );
1876 }
1877}