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";
728
729#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
731#[serde(deny_unknown_fields)]
732pub struct JsonPluginDescriptor {
733 pub abi: String,
734 pub capabilities: Vec<JsonCapabilityDescriptor>,
735 #[serde(default, skip_serializing_if = "Vec::is_empty")]
736 pub required_capabilities: Vec<JsonRequiredCapabilityDescriptor>,
737}
738
739#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
741#[serde(deny_unknown_fields)]
742pub struct JsonCapabilityDescriptor {
743 pub capability_id: String,
744 pub descriptor_version: String,
745 pub request_operations: Vec<String>,
746 #[serde(default, skip_serializing_if = "Vec::is_empty")]
747 pub stream_operations: Vec<String>,
748}
749
750#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
752#[serde(deny_unknown_fields)]
753pub struct JsonRequiredCapabilityDescriptor {
754 pub capability_id: String,
755 pub descriptor_version: String,
756 pub cardinality: CapabilityCardinality,
757}
758
759pub fn expected_json_plugin_descriptor(
761 instance: &PluginInstancePlan,
762) -> Result<JsonPluginDescriptor, RuntimeFailure> {
763 let mut capabilities = Vec::with_capacity(instance.provided_capabilities().len());
764 for descriptor in instance.provided_capabilities() {
765 if !descriptor.event_operations().is_empty() {
766 return Err(RuntimeFailure::InvalidResolvedPlan {
767 detail: format!(
768 "Execution class `{}` does not support Event endpoints",
769 instance.execution_class()
770 ),
771 });
772 }
773 capabilities.push(JsonCapabilityDescriptor {
774 capability_id: descriptor.capability_id().to_owned(),
775 descriptor_version: descriptor.descriptor_version().to_owned(),
776 request_operations: descriptor
777 .request_operations()
778 .into_iter()
779 .map(str::to_owned)
780 .collect(),
781 stream_operations: descriptor
782 .stream_operations()
783 .into_iter()
784 .map(str::to_owned)
785 .collect(),
786 });
787 }
788 capabilities.sort();
789 if capabilities
790 .windows(2)
791 .any(|pair| pair[0].capability_id == pair[1].capability_id)
792 {
793 return Err(RuntimeFailure::InvalidResolvedPlan {
794 detail: format!(
795 "Instance `{}` declares a duplicate Capability",
796 instance.instance_key()
797 ),
798 });
799 }
800 let mut required_capabilities = instance
801 .required_capabilities()
802 .iter()
803 .map(|requirement| JsonRequiredCapabilityDescriptor {
804 capability_id: requirement.capability_id().to_owned(),
805 descriptor_version: requirement.descriptor_version().to_owned(),
806 cardinality: requirement.cardinality(),
807 })
808 .collect::<Vec<_>>();
809 sort_required_capabilities(&mut required_capabilities);
810 Ok(JsonPluginDescriptor {
811 abi: if !required_capabilities.is_empty() {
812 JSON_HOST_IMPORTS_ABI_V1
813 } else if capabilities
814 .iter()
815 .any(|capability| !capability.stream_operations.is_empty())
816 {
817 JSON_INTERACTIONS_ABI_V1
818 } else {
819 JSON_REQUEST_ABI_V1
820 }
821 .to_owned(),
822 capabilities,
823 required_capabilities,
824 })
825}
826
827pub fn validate_json_plugin_descriptor(
829 instance: &PluginInstancePlan,
830 encoded: &str,
831) -> Result<(), RuntimeFailure> {
832 let mut actual = serde_json::from_str::<JsonPluginDescriptor>(encoded).map_err(|_| {
833 RuntimeFailure::ProtocolViolation {
834 capability: "lenso.json-request@1",
835 }
836 })?;
837 actual.capabilities.sort();
838 sort_required_capabilities(&mut actual.required_capabilities);
839 let expected = expected_json_plugin_descriptor(instance)?;
840 if actual != expected {
841 return Err(RuntimeFailure::InvalidResolvedPlan {
842 detail: format!(
843 "guest descriptor does not match resolved Instance `{}`",
844 instance.instance_key()
845 ),
846 });
847 }
848 Ok(())
849}
850
851fn sort_required_capabilities(requirements: &mut [JsonRequiredCapabilityDescriptor]) {
852 requirements.sort_by(|left, right| {
853 (
854 &left.capability_id,
855 &left.descriptor_version,
856 cardinality_order(left.cardinality),
857 )
858 .cmp(&(
859 &right.capability_id,
860 &right.descriptor_version,
861 cardinality_order(right.cardinality),
862 ))
863 });
864}
865
866const fn cardinality_order(cardinality: CapabilityCardinality) -> u8 {
867 match cardinality {
868 CapabilityCardinality::One => 0,
869 CapabilityCardinality::Optional => 1,
870 CapabilityCardinality::Many => 2,
871 }
872}
873
874pub trait JsonRequestTransport: std::fmt::Debug + 'static {
876 fn invoke(
877 self: Rc<Self>,
878 capability: String,
879 operation: String,
880 request_json: String,
881 context: InvocationContext,
882 ) -> futures::future::LocalBoxFuture<'static, Result<JsonInvocationOutcome, RuntimeFailure>>;
883}
884
885#[derive(Debug)]
887pub enum JsonStreamItem {
888 Message(Value),
889 PeerHalfClosed,
890 Terminal(Result<(), Value>),
891}
892
893#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
895#[serde(
896 tag = "kind",
897 content = "value",
898 rename_all = "kebab-case",
899 deny_unknown_fields
900)]
901pub enum JsonStreamFrame {
902 Message(Value),
903 PeerHalfClosed,
904 TerminalSuccess,
905 TerminalError(Value),
906}
907
908#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
910pub struct JsonHostBindingDescriptor {
911 pub binding_id: u32,
912 pub provider_instance: String,
913 pub capability_id: String,
914 pub descriptor_version: String,
915 pub request_operations: Vec<String>,
916 pub stream_operations: Vec<String>,
917}
918
919#[derive(Clone)]
920struct JsonHostBinding {
921 descriptor: JsonHostBindingDescriptor,
922 codec: Rc<dyn JsonCapabilityCodec>,
923 request: Option<PluginDependencyHandle>,
924 stream: Option<PluginStreamDependencyHandle>,
925}
926
927impl std::fmt::Debug for JsonHostBinding {
928 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
929 formatter
930 .debug_struct("JsonHostBinding")
931 .field("descriptor", &self.descriptor)
932 .finish_non_exhaustive()
933 }
934}
935
936#[derive(Debug)]
938pub struct JsonHostImports {
939 codecs: BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
940 bindings: std::cell::RefCell<Option<Vec<JsonHostBinding>>>,
941 streams: std::cell::RefCell<BTreeMap<u64, Rc<dyn JsonHostStreamSession>>>,
942 next_stream_id: std::cell::Cell<u64>,
943 max_streams: usize,
944}
945
946impl JsonHostImports {
947 pub fn new(
949 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
950 max_streams: usize,
951 ) -> Result<Self, RuntimeFailure> {
952 let mut by_capability = BTreeMap::new();
953 for codec in codecs {
954 let capability = codec.capability_id().to_owned();
955 if by_capability.insert(capability.clone(), codec).is_some() {
956 return Err(RuntimeFailure::InvalidResolvedPlan {
957 detail: format!("duplicate guest import codec for Capability `{capability}`"),
958 });
959 }
960 }
961 Ok(Self {
962 codecs: by_capability,
963 bindings: std::cell::RefCell::new(None),
964 streams: std::cell::RefCell::new(BTreeMap::new()),
965 next_stream_id: std::cell::Cell::new(1),
966 max_streams,
967 })
968 }
969
970 pub fn activate(&self, dependencies: &PluginDependencies) -> Result<(), RuntimeFailure> {
972 if self.bindings.borrow().is_some() {
973 return Err(RuntimeFailure::Internal {
974 detail: "guest Capability imports were activated twice".to_owned(),
975 });
976 }
977 let mut bindings = Vec::with_capacity(dependencies.len());
978 for (index, dependency) in dependencies.bindings().iter().enumerate() {
979 let codec = self
980 .codecs
981 .get(dependency.capability_id())
982 .cloned()
983 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
984 detail: format!(
985 "no generated guest import codec for Capability `{}`",
986 dependency.capability_id()
987 ),
988 })?;
989 let request = dependency.handle();
990 let stream = dependency.stream_handle();
991 validate_host_binding(&codec, request.as_ref(), stream.as_ref())?;
992 let binding_id =
993 u32::try_from(index).map_err(|_| RuntimeFailure::InvalidResolvedPlan {
994 detail: "guest import binding table exceeds u32 identity space".to_owned(),
995 })?;
996 bindings.push(JsonHostBinding {
997 descriptor: JsonHostBindingDescriptor {
998 binding_id,
999 provider_instance: dependency.provider_instance().to_owned(),
1000 capability_id: dependency.capability_id().to_owned(),
1001 descriptor_version: codec.descriptor_version().to_owned(),
1002 request_operations: request.as_ref().map_or_else(Vec::new, |handle| {
1003 handle
1004 .operations()
1005 .iter()
1006 .map(|item| (*item).to_owned())
1007 .collect()
1008 }),
1009 stream_operations: stream.as_ref().map_or_else(Vec::new, |handle| {
1010 handle
1011 .operations()
1012 .iter()
1013 .map(|item| (*item).to_owned())
1014 .collect()
1015 }),
1016 },
1017 codec,
1018 request,
1019 stream,
1020 });
1021 }
1022 self.bindings.replace(Some(bindings));
1023 Ok(())
1024 }
1025
1026 pub fn descriptors(&self) -> Result<Vec<JsonHostBindingDescriptor>, RuntimeFailure> {
1028 self.bindings
1029 .borrow()
1030 .as_ref()
1031 .map(|bindings| {
1032 bindings
1033 .iter()
1034 .map(|binding| binding.descriptor.clone())
1035 .collect()
1036 })
1037 .ok_or(RuntimeFailure::AdmissionClosed)
1038 }
1039
1040 pub fn invoke(
1042 &self,
1043 binding_id: u32,
1044 operation: String,
1045 request: Value,
1046 context: InvocationContext,
1047 ) -> JsonHostRequestFuture {
1048 let binding = match self.binding(binding_id) {
1049 Ok(binding) => binding,
1050 Err(error) => return Box::pin(futures::future::ready(Err(error))),
1051 };
1052 let Some(dependency) = binding.request else {
1053 return Box::pin(futures::future::ready(Err(
1054 RuntimeFailure::UnknownOperation {
1055 capability: binding.codec.capability_id(),
1056 operation,
1057 },
1058 )));
1059 };
1060 binding
1061 .codec
1062 .invoke_host_request(dependency, operation, request, context)
1063 }
1064
1065 pub fn open_stream(
1067 self: Rc<Self>,
1068 binding_id: u32,
1069 operation: String,
1070 request: Value,
1071 context: InvocationContext,
1072 ) -> futures::future::LocalBoxFuture<'static, Result<Result<u64, Value>, RuntimeFailure>> {
1073 Box::pin(async move {
1074 if self.streams.borrow().len() >= self.max_streams {
1075 return Err(RuntimeFailure::ResourceExhausted {
1076 capability: "lenso.json-host-imports@1",
1077 operation: "stream-open".to_owned(),
1078 });
1079 }
1080 let binding = self.binding(binding_id)?;
1081 let dependency = binding
1082 .stream
1083 .ok_or_else(|| RuntimeFailure::UnknownOperation {
1084 capability: binding.codec.capability_id(),
1085 operation: operation.clone(),
1086 })?;
1087 match binding
1088 .codec
1089 .open_host_stream(dependency, operation, request, context)
1090 .await?
1091 {
1092 Ok(stream) => {
1093 let stream_id = self.next_stream_id.get();
1094 let next =
1095 stream_id
1096 .checked_add(1)
1097 .ok_or(RuntimeFailure::ResourceExhausted {
1098 capability: "lenso.json-host-imports@1",
1099 operation: "stream-open".to_owned(),
1100 })?;
1101 self.next_stream_id.set(next);
1102 self.streams.borrow_mut().insert(stream_id, stream);
1103 Ok(Ok(stream_id))
1104 }
1105 Err(error) => Ok(Err(error)),
1106 }
1107 })
1108 }
1109
1110 pub fn send_stream(
1112 &self,
1113 stream_id: u64,
1114 message: Value,
1115 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1116 match self.stream(stream_id) {
1117 Ok(stream) => stream.send(message),
1118 Err(error) => Box::pin(futures::future::ready(Err(error))),
1119 }
1120 }
1121
1122 pub fn receive_stream(
1124 self: Rc<Self>,
1125 stream_id: u64,
1126 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>> {
1127 Box::pin(async move {
1128 let stream = self.stream(stream_id)?;
1129 let item = stream.receive().await?;
1130 if matches!(item, JsonStreamItem::Terminal(_)) {
1131 self.streams.borrow_mut().remove(&stream_id);
1132 }
1133 Ok(item)
1134 })
1135 }
1136
1137 pub fn close_stream_send(
1139 &self,
1140 stream_id: u64,
1141 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1142 match self.stream(stream_id) {
1143 Ok(stream) => stream.close_send(),
1144 Err(error) => Box::pin(futures::future::ready(Err(error))),
1145 }
1146 }
1147
1148 pub fn cancel_stream(&self, stream_id: u64) -> Result<(), RuntimeFailure> {
1150 let stream = self
1151 .streams
1152 .borrow_mut()
1153 .remove(&stream_id)
1154 .ok_or_else(unknown_host_stream)?;
1155 stream.cancel();
1156 Ok(())
1157 }
1158
1159 pub fn deactivate(&self) {
1161 self.bindings.replace(None);
1162 for (_, stream) in std::mem::take(&mut *self.streams.borrow_mut()) {
1163 stream.cancel();
1164 }
1165 }
1166
1167 fn binding(&self, binding_id: u32) -> Result<JsonHostBinding, RuntimeFailure> {
1168 let bindings = self.bindings.borrow();
1169 let bindings = bindings.as_ref().ok_or(RuntimeFailure::AdmissionClosed)?;
1170 bindings
1171 .get(binding_id as usize)
1172 .cloned()
1173 .ok_or(RuntimeFailure::ProtocolViolation {
1174 capability: JSON_HOST_IMPORTS_ABI_V1,
1175 })
1176 }
1177
1178 fn stream(&self, stream_id: u64) -> Result<Rc<dyn JsonHostStreamSession>, RuntimeFailure> {
1179 self.streams
1180 .borrow()
1181 .get(&stream_id)
1182 .cloned()
1183 .ok_or_else(unknown_host_stream)
1184 }
1185}
1186
1187fn validate_host_binding(
1188 codec: &Rc<dyn JsonCapabilityCodec>,
1189 request: Option<&PluginDependencyHandle>,
1190 stream: Option<&PluginStreamDependencyHandle>,
1191) -> Result<(), RuntimeFailure> {
1192 for (capability, version) in request
1193 .map(|handle| (handle.capability_id(), handle.descriptor_version()))
1194 .into_iter()
1195 .chain(stream.map(|handle| (handle.capability_id(), handle.descriptor_version())))
1196 {
1197 if capability != codec.capability_id() || version != codec.descriptor_version() {
1198 return Err(RuntimeFailure::ProtocolViolation {
1199 capability: codec.capability_id(),
1200 });
1201 }
1202 }
1203 Ok(())
1204}
1205
1206fn unknown_host_stream() -> RuntimeFailure {
1207 RuntimeFailure::ProtocolViolation {
1208 capability: "lenso.json-host-imports@1",
1209 }
1210}
1211
1212impl JsonStreamFrame {
1213 pub fn decode(
1215 encoded: &str,
1216 capability: &'static str,
1217 ) -> Result<JsonStreamItem, RuntimeFailure> {
1218 match serde_json::from_str(encoded)
1219 .map_err(|_| RuntimeFailure::ProtocolViolation { capability })?
1220 {
1221 Self::Message(value) => Ok(JsonStreamItem::Message(value)),
1222 Self::PeerHalfClosed => Ok(JsonStreamItem::PeerHalfClosed),
1223 Self::TerminalSuccess => Ok(JsonStreamItem::Terminal(Ok(()))),
1224 Self::TerminalError(value) => Ok(JsonStreamItem::Terminal(Err(value))),
1225 }
1226 }
1227}
1228
1229pub trait JsonStreamSessionTransport: std::fmt::Debug + 'static {
1231 fn send(
1232 self: Rc<Self>,
1233 message_json: String,
1234 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
1235 fn receive(
1236 self: Rc<Self>,
1237 ) -> futures::future::LocalBoxFuture<'static, Result<JsonStreamItem, RuntimeFailure>>;
1238 fn close_send(
1239 self: Rc<Self>,
1240 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
1241 fn cancel(&self);
1242}
1243
1244pub type JsonStreamOpenFuture = futures::future::LocalBoxFuture<
1246 'static,
1247 Result<Result<Rc<dyn JsonStreamSessionTransport>, Value>, RuntimeFailure>,
1248>;
1249
1250pub trait JsonStreamTransport: std::fmt::Debug + 'static {
1252 fn open(
1253 self: Rc<Self>,
1254 capability: String,
1255 operation: String,
1256 request_json: String,
1257 context: InvocationContext,
1258 ) -> JsonStreamOpenFuture;
1259}
1260
1261pub fn json_request_endpoints<T: JsonRequestTransport>(
1263 transport: Rc<T>,
1264 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
1265) -> Vec<Rc<dyn NativeRequestEndpoint>> {
1266 let transport: Rc<dyn JsonRequestTransport> = transport;
1267 codecs
1268 .into_iter()
1269 .filter(|codec| !codec.request_operations().is_empty())
1270 .map(|codec| {
1271 Rc::new(JsonRequestEndpoint {
1272 transport: transport.clone(),
1273 codec,
1274 }) as Rc<dyn NativeRequestEndpoint>
1275 })
1276 .collect()
1277}
1278
1279pub fn json_stream_endpoints<T: JsonStreamTransport>(
1281 transport: Rc<T>,
1282 codecs: Vec<Rc<dyn JsonCapabilityCodec>>,
1283) -> Vec<Rc<dyn NativeStreamEndpoint>> {
1284 let transport: Rc<dyn JsonStreamTransport> = transport;
1285 codecs
1286 .into_iter()
1287 .filter(|codec| !codec.stream_operations().is_empty())
1288 .map(|codec| {
1289 Rc::new(JsonStreamEndpoint {
1290 transport: transport.clone(),
1291 codec,
1292 }) as Rc<dyn NativeStreamEndpoint>
1293 })
1294 .collect()
1295}
1296
1297#[derive(Debug)]
1298struct JsonStreamEndpoint {
1299 transport: Rc<dyn JsonStreamTransport>,
1300 codec: Rc<dyn JsonCapabilityCodec>,
1301}
1302
1303impl NativeStreamEndpoint for JsonStreamEndpoint {
1304 fn capability_id(&self) -> &'static str {
1305 self.codec.capability_id()
1306 }
1307 fn descriptor_version(&self) -> &'static str {
1308 self.codec.descriptor_version()
1309 }
1310 fn operations(&self) -> &'static [&'static str] {
1311 self.codec.stream_operations()
1312 }
1313
1314 fn open(
1315 &self,
1316 operation: &str,
1317 request: Box<dyn Any>,
1318 context: InvocationContext,
1319 ) -> futures::future::LocalBoxFuture<
1320 'static,
1321 Result<Result<Box<dyn NativeStreamSession>, Box<dyn Any>>, RuntimeFailure>,
1322 > {
1323 let transport = self.transport.clone();
1324 let codec = self.codec.clone();
1325 let operation = operation.to_owned();
1326 Box::pin(async move {
1327 if !codec.stream_operations().contains(&operation.as_str()) {
1328 return Err(unknown_operation(codec.capability_id(), &operation));
1329 }
1330 let request = codec.encode_stream_open(&operation, request.as_ref())?;
1331 let request_json =
1332 serde_json::to_string(&request).map_err(|_| RuntimeFailure::ProtocolViolation {
1333 capability: codec.capability_id(),
1334 })?;
1335 match transport
1336 .open(
1337 codec.capability_id().to_owned(),
1338 operation.clone(),
1339 request_json,
1340 context,
1341 )
1342 .await?
1343 {
1344 Ok(session) => Ok(Ok(Box::new(JsonStreamSession {
1345 session,
1346 codec,
1347 operation,
1348 }) as Box<dyn NativeStreamSession>)),
1349 Err(error) => codec.decode_stream_domain_error(&operation, error).map(Err),
1350 }
1351 })
1352 }
1353}
1354
1355#[derive(Debug)]
1356struct JsonStreamSession {
1357 session: Rc<dyn JsonStreamSessionTransport>,
1358 codec: Rc<dyn JsonCapabilityCodec>,
1359 operation: String,
1360}
1361
1362impl NativeStreamSession for JsonStreamSession {
1363 fn send(
1364 &self,
1365 message: Box<dyn Any>,
1366 ) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1367 let encoded = self
1368 .codec
1369 .encode_stream_message(&self.operation, message.as_ref())
1370 .and_then(|value| {
1371 serde_json::to_string(&value).map_err(|_| RuntimeFailure::ProtocolViolation {
1372 capability: self.codec.capability_id(),
1373 })
1374 });
1375 let session = self.session.clone();
1376 Box::pin(async move { session.send(encoded?).await })
1377 }
1378
1379 fn receive(
1380 &self,
1381 ) -> futures::future::LocalBoxFuture<'static, Result<NativeStreamItem, RuntimeFailure>> {
1382 let session = self.session.clone();
1383 let codec = self.codec.clone();
1384 let operation = self.operation.clone();
1385 Box::pin(async move {
1386 match session.receive().await? {
1387 JsonStreamItem::Message(value) => codec
1388 .decode_stream_message(&operation, value)
1389 .map(NativeStreamItem::Message),
1390 JsonStreamItem::PeerHalfClosed => Ok(NativeStreamItem::PeerHalfClosed),
1391 JsonStreamItem::Terminal(Ok(())) => Ok(NativeStreamItem::Terminal(Ok(()))),
1392 JsonStreamItem::Terminal(Err(value)) => codec
1393 .decode_stream_domain_error(&operation, value)
1394 .map(|error| NativeStreamItem::Terminal(Err(error))),
1395 }
1396 })
1397 }
1398
1399 fn close_send(&self) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {
1400 self.session.clone().close_send()
1401 }
1402
1403 fn cancel(&self) {
1404 self.session.cancel();
1405 }
1406}
1407
1408#[derive(Debug)]
1409struct JsonRequestEndpoint {
1410 transport: Rc<dyn JsonRequestTransport>,
1411 codec: Rc<dyn JsonCapabilityCodec>,
1412}
1413
1414impl NativeRequestEndpoint for JsonRequestEndpoint {
1415 fn capability_id(&self) -> &'static str {
1416 self.codec.capability_id()
1417 }
1418
1419 fn descriptor_version(&self) -> &'static str {
1420 self.codec.descriptor_version()
1421 }
1422
1423 fn operations(&self) -> &'static [&'static str] {
1424 self.codec.request_operations()
1425 }
1426
1427 fn invoke(
1428 &self,
1429 operation: &str,
1430 request: Box<dyn Any>,
1431 context: InvocationContext,
1432 ) -> futures::future::LocalBoxFuture<
1433 'static,
1434 Result<Result<Box<dyn Any>, Box<dyn Any>>, RuntimeFailure>,
1435 > {
1436 let transport = self.transport.clone();
1437 let codec = self.codec.clone();
1438 let operation = operation.to_owned();
1439 Box::pin(async move {
1440 if !codec.request_operations().contains(&operation.as_str()) {
1441 return Err(RuntimeFailure::UnknownOperation {
1442 capability: codec.capability_id(),
1443 operation,
1444 });
1445 }
1446 let request = codec.encode_request(&operation, request.as_ref())?;
1447 let request =
1448 serde_json::to_string(&request).map_err(|_| RuntimeFailure::ProtocolViolation {
1449 capability: codec.capability_id(),
1450 })?;
1451 match transport
1452 .invoke(
1453 codec.capability_id().to_owned(),
1454 operation.clone(),
1455 request,
1456 context,
1457 )
1458 .await?
1459 {
1460 JsonInvocationOutcome::Success(value) => {
1461 codec.decode_response(&operation, value).map(Ok)
1462 }
1463 JsonInvocationOutcome::DomainError(value) => {
1464 codec.decode_domain_error(&operation, value).map(Err)
1465 }
1466 }
1467 })
1468 }
1469}
1470
1471pub fn codecs_for_instance(
1473 instance: &PluginInstancePlan,
1474 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1475) -> Result<Vec<Rc<dyn JsonCapabilityCodec>>, RuntimeFailure> {
1476 let mut selected = Vec::with_capacity(instance.provided_capabilities().len());
1477 for descriptor in instance.provided_capabilities() {
1478 if !descriptor.event_operations().is_empty() {
1479 return Err(RuntimeFailure::InvalidResolvedPlan {
1480 detail: format!(
1481 "Execution class `{}` does not support Event endpoints",
1482 instance.execution_class()
1483 ),
1484 });
1485 }
1486 let codec = codecs.get(descriptor.capability_id()).ok_or_else(|| {
1487 RuntimeFailure::InvalidResolvedPlan {
1488 detail: format!(
1489 "no generated codec for Capability `{}`",
1490 descriptor.capability_id()
1491 ),
1492 }
1493 })?;
1494 let request_operations: Vec<_> = codec
1495 .request_operations()
1496 .iter()
1497 .map(|operation| (*operation).to_owned())
1498 .collect();
1499 let stream_operations: Vec<_> = codec
1500 .stream_operations()
1501 .iter()
1502 .map(|operation| (*operation).to_owned())
1503 .collect();
1504 let expected_request: Vec<_> = descriptor
1505 .request_operations()
1506 .into_iter()
1507 .map(str::to_owned)
1508 .collect();
1509 let expected_stream: Vec<_> = descriptor
1510 .stream_operations()
1511 .into_iter()
1512 .map(str::to_owned)
1513 .collect();
1514 if codec.descriptor_version() != descriptor.descriptor_version()
1515 || request_operations != expected_request
1516 || stream_operations != expected_stream
1517 {
1518 return Err(RuntimeFailure::ProtocolViolation {
1519 capability: codec.capability_id(),
1520 });
1521 }
1522 selected.push(codec.clone());
1523 }
1524 Ok(selected)
1525}
1526
1527pub fn codecs_for_requirements(
1529 instance: &PluginInstancePlan,
1530 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1531) -> Result<Vec<Rc<dyn JsonCapabilityCodec>>, RuntimeFailure> {
1532 let mut selected = Vec::with_capacity(instance.required_capabilities().len());
1533 for requirement in instance.required_capabilities() {
1534 let codec = codecs.get(requirement.capability_id()).ok_or_else(|| {
1535 RuntimeFailure::InvalidResolvedPlan {
1536 detail: format!(
1537 "no generated guest import codec for Capability `{}`",
1538 requirement.capability_id()
1539 ),
1540 }
1541 })?;
1542 if codec.descriptor_version() != requirement.descriptor_version() {
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 prepare_request_app(
1554 plan: &ResolvedAppPlan,
1555 execution_class: &ExecutionClassId,
1556 generations: BTreeMap<String, PreparedNativePlugin>,
1557) -> Result<PreparedNativeApp, RuntimeFailure> {
1558 let selected_instances = plan
1559 .plugin_instances()
1560 .iter()
1561 .filter(|instance| instance.execution_class() == execution_class)
1562 .map(|instance| instance.instance_key().to_owned())
1563 .collect::<std::collections::BTreeSet<_>>();
1564 let mut endpoints = BTreeMap::new();
1565 let mut stream_endpoints = BTreeMap::new();
1566 for (instance_key, generation) in &generations {
1567 for endpoint in generation.endpoints() {
1568 let identity = (instance_key.clone(), endpoint.capability_id().to_owned());
1569 if endpoints.insert(identity, endpoint.clone()).is_some() {
1570 return Err(RuntimeFailure::InvalidResolvedPlan {
1571 detail: format!("duplicate request endpoint on Instance `{instance_key}`"),
1572 });
1573 }
1574 }
1575 for endpoint in generation.stream_endpoints() {
1576 let identity = (instance_key.clone(), endpoint.capability_id().to_owned());
1577 if stream_endpoints
1578 .insert(identity, endpoint.clone())
1579 .is_some()
1580 {
1581 return Err(RuntimeFailure::InvalidResolvedPlan {
1582 detail: format!("duplicate stream endpoint on Instance `{instance_key}`"),
1583 });
1584 }
1585 }
1586 }
1587 for instance in plan
1588 .plugin_instances()
1589 .iter()
1590 .filter(|instance| selected_instances.contains(instance.instance_key()))
1591 {
1592 if !generations.contains_key(instance.instance_key()) {
1593 return Err(RuntimeFailure::InvalidResolvedPlan {
1594 detail: format!("Adapter omitted Instance `{}`", instance.instance_key()),
1595 });
1596 }
1597 }
1598 let mut bindings = Vec::new();
1599 let mut stream_bindings = Vec::new();
1600 for binding in plan.capability_bindings() {
1601 let key = (
1602 binding.provider_instance().to_owned(),
1603 binding.capability_id().to_owned(),
1604 );
1605 let request_endpoint = endpoints.get(&key);
1606 let stream_endpoint = stream_endpoints.get(&key);
1607 if let Some(endpoint) = request_endpoint {
1608 bindings.push(PreparedBinding::new(
1609 binding.consumer_instance(),
1610 binding.provider_instance(),
1611 endpoint.clone(),
1612 ));
1613 }
1614 if let Some(endpoint) = stream_endpoint {
1615 stream_bindings.push(PreparedStreamBinding::new(
1616 binding.consumer_instance(),
1617 binding.provider_instance(),
1618 endpoint.clone(),
1619 ));
1620 }
1621 if request_endpoint.is_none()
1622 && stream_endpoint.is_none()
1623 && selected_instances.contains(binding.provider_instance())
1624 {
1625 return Err(RuntimeFailure::InvalidResolvedPlan {
1626 detail: format!(
1627 "Adapter omitted Capability `{}` endpoint for Instance `{}`",
1628 binding.capability_id(),
1629 binding.provider_instance()
1630 ),
1631 });
1632 }
1633 }
1634 Ok(PreparedNativeApp::new(bindings, generations).with_stream_bindings(stream_bindings))
1635}
1636
1637pub fn require_operation(
1639 codecs: &BTreeMap<String, Rc<dyn JsonCapabilityCodec>>,
1640 capability_id: &str,
1641 operation: &str,
1642) -> Result<Rc<dyn JsonCapabilityCodec>, RuntimeFailure> {
1643 let codec =
1644 codecs
1645 .get(capability_id)
1646 .cloned()
1647 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
1648 detail: format!("no generated codec for Capability `{capability_id}`"),
1649 })?;
1650 if !codec.request_operations().contains(&operation) {
1651 return Err(RuntimeFailure::UnknownOperation {
1652 capability: codec.capability_id(),
1653 operation: operation.to_owned(),
1654 });
1655 }
1656 Ok(codec)
1657}
1658
1659fn unknown_operation(capability: &'static str, operation: &str) -> RuntimeFailure {
1660 RuntimeFailure::UnknownOperation {
1661 capability,
1662 operation: operation.to_owned(),
1663 }
1664}
1665
1666fn validate_digest(digest: &str) -> Result<(), RuntimeFailure> {
1667 let valid = digest.strip_prefix("sha256:").is_some_and(|hex| {
1668 hex.len() == 64
1669 && hex
1670 .bytes()
1671 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
1672 });
1673 if valid {
1674 Ok(())
1675 } else {
1676 Err(RuntimeFailure::InvalidResolvedPlan {
1677 detail: format!("invalid canonical SHA-256 digest `{digest}`"),
1678 })
1679 }
1680}
1681
1682fn invalid_artifact(path: &Path, error: impl std::fmt::Display) -> RuntimeFailure {
1683 RuntimeFailure::InvalidResolvedPlan {
1684 detail: format!("cannot read Artifact `{}`: {error}", path.display()),
1685 }
1686}
1687
1688fn validate_resource_path(path: &str) -> Result<(), RuntimeFailure> {
1689 if path.is_empty()
1690 || path.starts_with('/')
1691 || path.contains(['\\', '\0'])
1692 || path
1693 .split('/')
1694 .any(|segment| segment.is_empty() || matches!(segment, "." | ".."))
1695 {
1696 return Err(invalid_resources(format!(
1697 "invalid Plugin resource path `{path}`"
1698 )));
1699 }
1700 Ok(())
1701}
1702
1703fn invalid_resources(detail: impl Into<String>) -> RuntimeFailure {
1704 RuntimeFailure::InvalidResolvedPlan {
1705 detail: detail.into(),
1706 }
1707}
1708
1709#[cfg(test)]
1710mod tests {
1711 use std::io::Write;
1712
1713 use super::*;
1714
1715 #[test]
1716 fn artifact_handle_keeps_the_admitted_bytes_after_source_drift() {
1717 let mut file = tempfile::NamedTempFile::new().unwrap();
1718 file.write_all(b"first").unwrap();
1719 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"first")));
1720 let handle = ArtifactHandle::open(file.path(), &digest, 5).unwrap();
1721 file.as_file_mut().set_len(0).unwrap();
1722 file.write_all(b"other").unwrap();
1723 assert_eq!(handle.read_verified().unwrap(), b"first");
1724 assert_ne!(handle.path(), file.path());
1725 assert_eq!(fs::read(handle.path()).unwrap(), b"first");
1726 }
1727
1728 #[test]
1729 fn artifact_snapshot_survives_source_parent_rename_and_replacement() {
1730 let workspace = tempfile::tempdir().unwrap();
1731 let selected = workspace.path().join("selected");
1732 fs::create_dir(&selected).unwrap();
1733 let source = selected.join("plugin");
1734 fs::write(&source, b"admitted").unwrap();
1735 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"admitted")));
1736
1737 let handle = ArtifactHandle::open(&source, &digest, 8).unwrap();
1738 assert!(!handle.path().starts_with(&selected));
1739 fs::rename(&selected, workspace.path().join("replaced")).unwrap();
1740 fs::create_dir(&selected).unwrap();
1741 fs::write(selected.join("plugin"), b"attacker").unwrap();
1742
1743 assert_eq!(fs::read(handle.path()).unwrap(), b"admitted");
1744 assert_eq!(handle.read_verified().unwrap(), b"admitted");
1745 }
1746
1747 #[test]
1748 fn artifact_admission_streams_large_content_into_one_stable_snapshot() {
1749 let bytes = vec![0x5a; 4 * 1024 * 1024 + 17];
1750 let mut file = tempfile::NamedTempFile::new().unwrap();
1751 file.write_all(&bytes).unwrap();
1752 let digest = format!("sha256:{}", hex::encode(Sha256::digest(&bytes)));
1753
1754 let handle = ArtifactHandle::open(file.path(), &digest, bytes.len() as u64).unwrap();
1755
1756 assert_eq!(handle.read_verified().unwrap(), bytes);
1757 }
1758
1759 #[test]
1762 #[ignore = "large Artifact admission benchmark; run explicitly"]
1763 fn artifact_admission_streaming_benchmark() {
1764 const BLOCK_BYTES: usize = 64 * 1024;
1765 let block = vec![0x5a; BLOCK_BYTES];
1766 for mebibytes in [4_usize, 64, 256] {
1767 let directory = tempfile::tempdir().unwrap();
1768 let path = directory.path().join("artifact");
1769 let mut source = fs::File::create(&path).unwrap();
1770 let mut hasher = Sha256::new();
1771 let blocks = mebibytes * 1024 * 1024 / BLOCK_BYTES;
1772 for _ in 0..blocks {
1773 source.write_all(&block).unwrap();
1774 hasher.update(&block);
1775 }
1776 drop(source);
1777 let size = u64::try_from(mebibytes * 1024 * 1024).unwrap();
1778 let digest = format!("sha256:{}", hex::encode(hasher.finalize()));
1779
1780 let started = std::time::Instant::now();
1781 let handle = ArtifactHandle::open(&path, &digest, size).unwrap();
1782 let elapsed = started.elapsed();
1783
1784 assert_eq!(handle.size(), size);
1785 println!(
1786 "{{\"mebibytes\":{mebibytes},\"elapsed_ms\":{:.3},\"mib_per_second\":{:.3}}}",
1787 elapsed.as_secs_f64() * 1_000.0,
1788 f64::from(u32::try_from(mebibytes).unwrap()) / elapsed.as_secs_f64()
1789 );
1790 drop(handle);
1791 }
1792 }
1793
1794 #[test]
1795 fn artifact_admission_honors_an_explicit_host_staging_root() {
1796 let source = tempfile::tempdir().unwrap();
1797 let staging = tempfile::tempdir().unwrap();
1798 let path = source.path().join("plugin");
1799 fs::write(&path, b"artifact").unwrap();
1800 let digest = format!("sha256:{}", hex::encode(Sha256::digest(b"artifact")));
1801
1802 let handle =
1803 ArtifactHandle::open_with_staging_root(&path, &digest, 8, staging.path()).unwrap();
1804
1805 assert_eq!(
1806 handle.path().parent().unwrap().parent().unwrap(),
1807 staging.path()
1808 );
1809 }
1810
1811 #[test]
1812 fn instance_resources_are_order_independent_and_immutable() {
1813 let left = InstanceResources::from_files([
1814 ("prompts/system.md".to_owned(), b"Build carefully.".to_vec()),
1815 ("rules.toml".to_owned(), b"turns = 4\n".to_vec()),
1816 ])
1817 .unwrap();
1818 let right = InstanceResources::from_files([
1819 ("rules.toml".to_owned(), b"turns = 4\n".to_vec()),
1820 ("prompts/system.md".to_owned(), b"Build carefully.".to_vec()),
1821 ])
1822 .unwrap();
1823
1824 assert_eq!(left.digest(), right.digest());
1825 assert_eq!(
1826 left.read_text("prompts/system.md").unwrap(),
1827 "Build carefully."
1828 );
1829 assert_eq!(left.file_count(), 2);
1830 assert_eq!(left.total_size(), 26);
1831 }
1832
1833 #[test]
1834 fn instance_resources_reject_escaping_and_duplicate_paths() {
1835 assert!(InstanceResources::from_files([("../secret".to_owned(), Vec::new())]).is_err());
1836 assert!(
1837 InstanceResources::from_files([
1838 ("rules.toml".to_owned(), Vec::new()),
1839 ("rules.toml".to_owned(), Vec::new()),
1840 ])
1841 .is_err()
1842 );
1843 }
1844}