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    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/// Immutable, content-addressed files owned by one Plugin Instance Generation.
27#[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    /// Builds one deterministic snapshot from normalized relative paths and owned bytes.
42    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    /// Returns the deterministic identity of every path and byte in this snapshot.
85    pub fn digest(&self) -> &str {
86        &self.digest
87    }
88
89    /// Returns the number of snapshotted files.
90    pub fn file_count(&self) -> usize {
91        self.files.len()
92    }
93
94    /// Returns the aggregate byte size.
95    pub const fn total_size(&self) -> u64 {
96        self.total_size
97    }
98
99    /// Lists normalized paths in deterministic order.
100    pub fn paths(&self) -> impl Iterator<Item = &str> {
101        self.files.keys().map(String::as_str)
102    }
103
104    /// Reads one immutable resource without consulting the live filesystem.
105    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    /// Reads one immutable UTF-8 resource.
114    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/// Immutable Instance-to-resource-snapshot mapping injected by the Generation Supervisor.
121#[derive(Clone, Debug, Default)]
122pub struct InstanceResourceCatalog {
123    snapshots: BTreeMap<String, InstanceResources>,
124    empty: InstanceResources,
125}
126
127impl InstanceResourceCatalog {
128    /// Creates an empty catalog.
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    /// Adds one exact Instance snapshot and rejects duplicate authority.
134    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    /// Returns the selected snapshot or an immutable empty snapshot.
153    pub fn for_instance(&self, instance_key: &str) -> &InstanceResources {
154        self.snapshots.get(instance_key).unwrap_or(&self.empty)
155    }
156
157    /// Iterates selected Instance snapshots in deterministic order.
158    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/// Digest-verified, read-only execution input selected before Adapter preparation.
166#[derive(Debug)]
167struct ArtifactBacking {
168    path: PathBuf,
169    _directory: tempfile::TempDir,
170}
171
172/// One immutable content snapshot captured during Artifact admission.
173#[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    /// Verifies one regular file and snapshots it in a process-private directory selected by the
205    /// Host's system-temporary policy, independently of the source path. Strict isolation or
206    /// path-based execution on a no-exec temporary filesystem should use
207    /// [`Self::open_with_staging_root`] with a Host-owned executable root.
208    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    /// Verifies one Artifact and places its private stable copy under an explicit Host-owned root.
217    /// Process-capable Hosts should select a root on a filesystem that permits execution.
218    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    /// Returns the private stable copy containing the bytes admitted by this Handle.
286    /// It is never serialized into a Plan.
287    pub fn path(&self) -> &Path {
288        &self.backing.path
289    }
290
291    /// Returns the original machine-local selection path for relative resource policy.
292    pub fn source_path(&self) -> &Path {
293        &self.source_path
294    }
295
296    /// Returns the verified content identity.
297    pub fn digest(&self) -> &str {
298        &self.digest
299    }
300
301    /// Returns the verified byte size.
302    pub const fn size(&self) -> u64 {
303        self.size
304    }
305
306    /// Returns the exact bytes captured during admission.
307    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/// Immutable Instance-to-Artifact mapping injected by the Generation Supervisor.
417#[derive(Clone, Debug, Default)]
418pub struct ArtifactCatalog(BTreeMap<String, ArtifactHandle>);
419
420impl ArtifactCatalog {
421    /// Creates an empty catalog for an Adapter with no selected Instances.
422    pub fn new() -> Self {
423        Self::default()
424    }
425
426    /// Adds one exact execution input and rejects duplicate Instance authority.
427    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    /// Resolves the one selected execution input for an Instance.
442    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
451/// Generated typed-value bridge shared by byte-oriented Execution Adapters.
452pub trait JsonCapabilityCodec: std::fmt::Debug + 'static {
453    /// Stable Capability series identity.
454    fn capability_id(&self) -> &'static str;
455    /// Exact Descriptor version.
456    fn descriptor_version(&self) -> &'static str;
457    /// Exact request Operation table.
458    fn request_operations(&self) -> &'static [&'static str];
459    /// Exact bidirectional stream Operation table.
460    fn stream_operations(&self) -> &'static [&'static str] {
461        &[]
462    }
463    /// Converts one generated request into validated portable JSON.
464    fn encode_request(&self, operation: &str, request: &dyn Any) -> Result<Value, RuntimeFailure>;
465    /// Converts portable JSON into the generated response value.
466    fn decode_response(
467        &self,
468        operation: &str,
469        value: Value,
470    ) -> Result<Box<dyn Any>, RuntimeFailure>;
471    /// Converts portable JSON into the generated Domain Error value.
472    fn decode_domain_error(
473        &self,
474        operation: &str,
475        value: Value,
476    ) -> Result<Box<dyn Any>, RuntimeFailure>;
477    /// Converts one generated stream-open request into validated portable JSON.
478    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    /// Converts one generated outbound stream message into validated portable JSON.
487    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    /// Converts one portable JSON stream message into its generated value.
496    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    /// Converts one portable JSON stream terminal error into its generated value.
505    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    /// Invokes one exact Plan-bound host Request dependency from portable JSON.
514    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    /// Opens one exact Plan-bound host Stream dependency from portable JSON.
528    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/// Exact host outcome returned by a byte-oriented Plugin invocation.
544#[derive(Debug)]
545pub enum JsonInvocationOutcome {
546    /// Successful generated response value.
547    Success(Value),
548    /// Declared generated Domain Error value.
549    DomainError(Value),
550}
551
552/// Projects a Runtime Failure into a bounded, secret-free guest ABI value.
553pub 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
607/// Encodes a host import Request result into the stable guest envelope.
608pub 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
618/// Result of one Plan-bound host Request import after generated value translation.
619pub type JsonHostRequestFuture =
620    futures::future::LocalBoxFuture<'static, Result<JsonInvocationOutcome, RuntimeFailure>>;
621
622/// Adapter-neutral host Stream session exposed to a byte-oriented guest.
623pub 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
637/// Result of opening one Plan-bound host Stream import.
638pub 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
650/// Wraps one generated typed host Stream as portable JSON for a guest import.
651pub 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
720/// Stable request-only guest ABI implemented by byte-oriented Plugin runtimes.
721pub const JSON_REQUEST_ABI_V1: &str = "lenso.json-request@1";
722
723/// Stable Request and bidirectional Stream guest ABI.
724pub const JSON_INTERACTIONS_ABI_V1: &str = "lenso.json-interactions@1";
725
726/// Stable Request, Stream, and Plan-bound host Capability import ABI.
727pub const JSON_HOST_IMPORTS_ABI_V1: &str = "lenso.json-host-imports@1";
728
729/// Exact guest declaration returned before an Adapter opens readiness.
730#[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/// One exact request Capability exposed by a guest Plugin.
740#[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/// One exact Capability requirement declared by a guest Plugin.
751#[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
759/// Derives the only guest declaration accepted for one resolved Instance.
760pub 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
827/// Parses and compares a guest Ready declaration with exact Plan authority.
828pub 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
874/// Guest transport seam shared by Wasm Component and embedded-JavaScript Adapters.
875pub 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/// One exact transport frame received from a byte-oriented guest stream.
886#[derive(Debug)]
887pub enum JsonStreamItem {
888    Message(Value),
889    PeerHalfClosed,
890    Terminal(Result<(), Value>),
891}
892
893/// Canonical portable JSON frame returned by `stream-receive` guest exports.
894#[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/// One exact Plan binding exposed to a guest Plugin after lifecycle activation.
909#[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/// Activated, Plan-bound Capability imports for one byte-oriented guest generation.
937#[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    /// Creates a closed import table from the exact generated requirement codecs.
948    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    /// Installs only the dependencies materialized from the immutable Plan.
971    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    /// Returns the exact activated binding table in resolved provider order.
1027    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    /// Invokes one activated Request binding by its unforgeable table index.
1041    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    /// Opens one activated Stream binding and assigns an Adapter-local import id.
1066    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    /// Sends one portable message through a guest-owned host Stream.
1111    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    /// Receives the next portable frame from one guest-owned host Stream.
1123    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    /// Half-closes the guest-to-host direction of one guest-owned host Stream.
1138    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    /// Cancels and removes one guest-owned host Stream.
1149    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    /// Closes admission and cancels every import Stream owned by this generation.
1160    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    /// Parses one bounded guest result into the Adapter-neutral transport item.
1214    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
1229/// Adapter-owned transport session for the portable JSON Stream ABI.
1230pub 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
1244/// Adapter-owned result of opening one portable JSON stream transport session.
1245pub type JsonStreamOpenFuture = futures::future::LocalBoxFuture<
1246    'static,
1247    Result<Result<Rc<dyn JsonStreamSessionTransport>, Value>, RuntimeFailure>,
1248>;
1249
1250/// Guest transport seam shared by Stream-capable byte-oriented Adapters.
1251pub 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
1261/// Builds typed Kernel endpoints over one exact guest transport generation.
1262pub 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
1279/// Builds typed Kernel Stream endpoints over one exact guest transport generation.
1280pub 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
1471/// Validates Plan descriptors against registered generated codecs.
1472pub 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
1527/// Validates every declared guest requirement against one registered generated codec.
1528pub 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
1552/// Builds exact request bindings from Adapter-prepared Plugin generations.
1553pub 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
1637/// Looks up the exact codec and validates the Operation before dispatch.
1638pub 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    /// Reproducible evidence command:
1760    /// `cargo test --release -p lenso-runtime-codec artifact_admission_streaming_benchmark -- --ignored --nocapture`
1761    #[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}