Skip to main content

lenso_runtime_codec/
lib.rs

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