Skip to main content

lash_lashlang_runtime/
lib.rs

1use std::sync::{Arc, Mutex};
2
3use sha2::{Digest, Sha256};
4
5pub use lash_trace::{
6    TraceLashlangChildExecution, TraceLashlangEdgeSelection, TraceLashlangExecutionEvent,
7    TraceLashlangExecutionIdentity, TraceLashlangGraph, TraceLashlangGraphChildLink,
8    TraceLashlangGraphEdge, TraceLashlangGraphNode, TraceLashlangGraphStore, TraceLashlangMap,
9    TraceLashlangMapEdge, TraceLashlangMapNode, TraceLashlangNodeStatus, TraceLashlangStatus,
10};
11pub use lashlang::{
12    CompiledProcessCache, DurabilityTier as LashlangDurabilityTier, InMemoryLashlangArtifactStore,
13    LASH_TYPE_KEY, LashlangAbilities, LashlangArtifactStore, LashlangHostCatalog,
14    LashlangHostEnvironment, LashlangLanguageFeatures,
15};
16
17pub const LASHLANG_ENGINE_KIND: &str = "lashlang";
18pub const LASHLANG_TOOL_BINDING_KEY: &str = "lashlang.tool";
19pub const LASHLANG_SURFACE_EXTENSION_ID: &str = "lashlang.surface";
20
21#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22#[serde(default)]
23pub struct LashlangSurfaceContribution {
24    pub abilities: LashlangAbilities,
25    pub language_features: LashlangLanguageFeatures,
26    pub resources: LashlangHostCatalog,
27}
28
29impl LashlangSurfaceContribution {
30    pub fn new(
31        abilities: LashlangAbilities,
32        language_features: LashlangLanguageFeatures,
33        resources: LashlangHostCatalog,
34    ) -> Self {
35        Self {
36            abilities,
37            language_features,
38            resources,
39        }
40    }
41
42    pub fn from_surface(surface: LashlangSurface) -> Self {
43        Self {
44            abilities: surface.abilities,
45            language_features: surface.language_features,
46            resources: surface.resources,
47        }
48    }
49}
50
51#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52pub struct LashlangToolBinding {
53    #[serde(default, skip_serializing_if = "Vec::is_empty")]
54    pub module_path: Vec<String>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub operation: Option<String>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub authority_type: Option<String>,
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub aliases: Vec<String>,
61}
62
63impl LashlangToolBinding {
64    pub fn new(
65        module_path: impl IntoIterator<Item = impl Into<String>>,
66        operation: impl Into<String>,
67    ) -> Self {
68        Self {
69            module_path: module_path.into_iter().map(Into::into).collect(),
70            operation: Some(operation.into()),
71            authority_type: None,
72            aliases: Vec::new(),
73        }
74    }
75
76    pub fn with_authority_type(mut self, authority_type: impl Into<String>) -> Self {
77        self.authority_type = Some(authority_type.into());
78        self
79    }
80
81    pub fn with_aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
82        self.aliases = aliases.into_iter().map(Into::into).collect();
83        self
84    }
85
86    pub fn executable_for(&self, tool_name: &str) -> ResolvedLashlangToolBinding {
87        let module_path = if self.module_path.is_empty() {
88            vec!["tools".to_string()]
89        } else {
90            self.module_path.clone()
91        };
92        let operation = self
93            .operation
94            .as_deref()
95            .filter(|operation| !operation.trim().is_empty())
96            .map(ToOwned::to_owned)
97            .unwrap_or_else(|| tool_name.replace('_', "."));
98        let authority_type = self
99            .authority_type
100            .as_deref()
101            .filter(|authority_type| !authority_type.trim().is_empty())
102            .map(ToOwned::to_owned)
103            .unwrap_or_else(|| default_authority_type(&module_path));
104        ResolvedLashlangToolBinding {
105            module_path,
106            operation,
107            authority_type,
108            aliases: self.aliases.clone(),
109        }
110    }
111
112    pub fn required_for_remote(
113        manifest: &lash_core::ToolManifest,
114    ) -> Result<ResolvedLashlangToolBinding, String> {
115        tool_lashlang_binding(manifest).required_executable_for_remote(&manifest.name)
116    }
117
118    pub fn required_executable_for_remote(
119        &self,
120        tool_name: &str,
121    ) -> Result<ResolvedLashlangToolBinding, String> {
122        if self.module_path.is_empty() {
123            return Err(format!(
124                "tool `{tool_name}` is missing an explicit remote module path"
125            ));
126        }
127        let operation = self
128            .operation
129            .as_deref()
130            .filter(|operation| !operation.trim().is_empty())
131            .ok_or_else(|| {
132                format!("tool `{tool_name}` is missing an explicit remote operation name")
133            })?;
134        let authority_type = self
135            .authority_type
136            .as_deref()
137            .filter(|authority_type| !authority_type.trim().is_empty())
138            .map(ToOwned::to_owned)
139            .unwrap_or_else(|| default_authority_type(&self.module_path));
140        Ok(ResolvedLashlangToolBinding {
141            module_path: self.module_path.clone(),
142            operation: operation.to_string(),
143            authority_type,
144            aliases: self.aliases.clone(),
145        })
146    }
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct ResolvedLashlangToolBinding {
151    pub module_path: Vec<String>,
152    pub operation: String,
153    pub authority_type: String,
154    pub aliases: Vec<String>,
155}
156
157impl ResolvedLashlangToolBinding {
158    pub fn module_path_string(&self) -> String {
159        self.module_path.join(".")
160    }
161
162    pub fn call_path(&self) -> String {
163        format!("{}.{}", self.module_path_string(), self.operation)
164    }
165}
166
167fn default_authority_type(module_path: &[String]) -> String {
168    module_path
169        .last()
170        .map(|segment| {
171            let mut chars = segment.chars();
172            match chars.next() {
173                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
174                None => "Tool".to_string(),
175            }
176        })
177        .unwrap_or_else(|| "Tool".to_string())
178}
179
180pub fn tool_lashlang_binding(manifest: &lash_core::ToolManifest) -> LashlangToolBinding {
181    manifest
182        .bindings
183        .get(LASHLANG_TOOL_BINDING_KEY)
184        .cloned()
185        .and_then(|value| serde_json::from_value(value).ok())
186        .unwrap_or_default()
187}
188
189pub trait ToolManifestLashlangExt {
190    fn lashlang_binding(&self) -> Result<Option<LashlangToolBinding>, serde_json::Error>;
191}
192
193impl ToolManifestLashlangExt for lash_core::ToolManifest {
194    fn lashlang_binding(&self) -> Result<Option<LashlangToolBinding>, serde_json::Error> {
195        self.bindings
196            .get(LASHLANG_TOOL_BINDING_KEY)
197            .cloned()
198            .map(serde_json::from_value)
199            .transpose()
200    }
201}
202
203pub trait ToolDefinitionLashlangExt {
204    fn with_lashlang_binding(self, lashlang_binding: LashlangToolBinding) -> Self;
205}
206
207impl ToolDefinitionLashlangExt for lash_core::ToolDefinition {
208    fn with_lashlang_binding(mut self, lashlang_binding: LashlangToolBinding) -> Self {
209        let value = serde_json::to_value(lashlang_binding)
210            .expect("lashlang tool binding must serialize to JSON");
211        self.manifest
212            .bindings
213            .insert(LASHLANG_TOOL_BINDING_KEY.to_string(), value);
214        self
215    }
216}
217
218pub trait RemoteToolGrantLashlangExt {
219    fn with_lashlang_binding(self, lashlang_binding: LashlangToolBinding) -> Self;
220    fn lashlang_binding(&self) -> Result<Option<LashlangToolBinding>, serde_json::Error>;
221}
222
223impl RemoteToolGrantLashlangExt for lash_remote_protocol::RemoteToolGrant {
224    fn with_lashlang_binding(mut self, lashlang_binding: LashlangToolBinding) -> Self {
225        let value = serde_json::to_value(lashlang_binding)
226            .expect("lashlang tool binding must serialize to JSON");
227        self.bindings
228            .insert(LASHLANG_TOOL_BINDING_KEY.to_string(), value);
229        self
230    }
231
232    fn lashlang_binding(&self) -> Result<Option<LashlangToolBinding>, serde_json::Error> {
233        self.bindings
234            .get(LASHLANG_TOOL_BINDING_KEY)
235            .cloned()
236            .map(serde_json::from_value)
237            .transpose()
238    }
239}
240
241#[derive(Clone, Debug)]
242pub struct LashlangSurface {
243    pub abilities: LashlangAbilities,
244    pub language_features: LashlangLanguageFeatures,
245    pub resources: LashlangHostCatalog,
246}
247
248impl Default for LashlangSurface {
249    fn default() -> Self {
250        Self {
251            abilities: LashlangAbilities::default().with_sleep(),
252            language_features: LashlangLanguageFeatures::default(),
253            resources: LashlangHostCatalog::new(),
254        }
255    }
256}
257
258impl LashlangSurface {
259    pub fn new(
260        abilities: LashlangAbilities,
261        language_features: LashlangLanguageFeatures,
262        resources: LashlangHostCatalog,
263    ) -> Self {
264        Self {
265            abilities,
266            language_features,
267            resources,
268        }
269    }
270
271    pub fn for_process_registry(mut self, process_registry_available: bool) -> Self {
272        self.abilities = self.abilities.with_sleep();
273        if process_registry_available {
274            self.abilities = self.abilities.with_processes().with_process_signals();
275        } else {
276            self.abilities.processes = false;
277            self.abilities.process_signals = false;
278        }
279        self
280    }
281
282    pub fn with_resources(mut self, resources: LashlangHostCatalog) -> Self {
283        self.resources.extend(resources);
284        self
285    }
286
287    pub fn with_plugin_extensions(
288        mut self,
289        extensions: &lash_core::PluginExtensions,
290    ) -> Result<Self, String> {
291        for payload in extensions.payloads(LASHLANG_SURFACE_EXTENSION_ID) {
292            let contribution: LashlangSurfaceContribution = serde_json::from_value(payload.clone())
293                .map_err(|err| {
294                    format!("invalid `{LASHLANG_SURFACE_EXTENSION_ID}` extension payload: {err}")
295                })?;
296            self.abilities = self.abilities.union(contribution.abilities);
297            self.language_features = self.language_features.union(contribution.language_features);
298            self.resources.extend(contribution.resources);
299        }
300        Ok(self)
301    }
302
303    pub fn host_environment(&self, catalog: &lash_core::ToolCatalog) -> LashlangHostEnvironment {
304        lashlang_host_environment_from_tool_catalog(
305            catalog,
306            self.abilities,
307            self.language_features,
308            self.resources.clone(),
309        )
310    }
311}
312
313pub fn lashlang_host_environment_from_tool_catalog(
314    catalog: &lash_core::ToolCatalog,
315    abilities: LashlangAbilities,
316    language_features: LashlangLanguageFeatures,
317    host_resources: LashlangHostCatalog,
318) -> LashlangHostEnvironment {
319    let mut resources = lashlang_resources_from_tool_catalog(catalog);
320    resources.extend(host_resources);
321    if abilities.triggers {
322        lashlang::add_trigger_resource_operations(&mut resources);
323    }
324    LashlangHostEnvironment::new(resources, abilities).with_language_features(language_features)
325}
326
327pub fn lashlang_resources_from_tool_catalog(
328    catalog: &lash_core::ToolCatalog,
329) -> LashlangHostCatalog {
330    let mut host_catalog = LashlangHostCatalog::new();
331    for entry in catalog.tools.iter() {
332        if entry.availability.is_callable() {
333            let lashlang_binding =
334                tool_lashlang_binding(&entry.manifest).executable_for(&entry.manifest.name);
335            host_catalog.add_module_operation(
336                lashlang_binding.module_path.iter().map(String::as_str),
337                lashlang_binding.authority_type.clone(),
338                lashlang_binding.operation.clone(),
339                entry.manifest.id.to_string(),
340                lashlang::TypeExpr::Any,
341                lashlang::TypeExpr::Any,
342            );
343        }
344    }
345    host_catalog
346}
347
348pub fn lashlang_host_environment_satisfies_requirements(
349    required: &lashlang::HostRequirements,
350    current: &LashlangHostEnvironment,
351) -> Result<(), String> {
352    let abilities = required.abilities;
353    let current_abilities = current.abilities;
354    if abilities.processes && !current_abilities.processes {
355        return Err("processes are not available".to_string());
356    }
357    if abilities.sleep && !current_abilities.sleep {
358        return Err("sleep is not available".to_string());
359    }
360    if abilities.process_signals && !current_abilities.process_signals {
361        return Err("process signals are not available".to_string());
362    }
363    if abilities.triggers && !current_abilities.triggers {
364        return Err("triggers are not available".to_string());
365    }
366    if required.language_features.label_annotations && !current.language_features.label_annotations
367    {
368        return Err("label annotations are not available".to_string());
369    }
370
371    for (_, module) in required.resources.module_instances() {
372        let current_module = current
373            .resources
374            .resolve_module_path(&module.path)
375            .ok_or_else(|| format!("module `{}` is not available", module.alias))?;
376        if current_module.resource_type != module.resource_type {
377            return Err(format!(
378                "module `{}` has type `{}`, expected `{}`",
379                module.alias, current_module.resource_type, module.resource_type
380            ));
381        }
382        for (operation, required_binding) in &module.operations {
383            match current.resources.resolve_module_operation(
384                &module.resource_type,
385                &module.alias,
386                operation,
387            ) {
388                Some(current_binding) if current_binding == required_binding => {}
389                Some(current_binding) => {
390                    return Err(format!(
391                        "module `{}` operation `{operation}` resolves to `{}`, expected `{}`",
392                        module.alias,
393                        current_binding.host_operation,
394                        required_binding.host_operation
395                    ));
396                }
397                None => {
398                    return Err(format!(
399                        "module `{}` does not expose operation `{operation}`",
400                        module.alias
401                    ));
402                }
403            }
404        }
405    }
406
407    for (resource_type, required_type) in required.resources.resource_types() {
408        if !current.resources.has_resource_type(resource_type) {
409            return Err(format!("resource type `{resource_type}` is not available"));
410        }
411        for (operation, required_binding) in &required_type.operations {
412            let current_binding = current
413                .resources
414                .resolve_operation(resource_type, operation)
415                .ok_or_else(|| {
416                    format!(
417                        "resource type `{resource_type}` does not expose operation `{operation}`"
418                    )
419                })?;
420            if current_binding.input_ty != required_binding.input_ty {
421                return Err(format!(
422                    "resource type `{resource_type}` operation `{operation}` has incompatible input type"
423                ));
424            }
425            if current_binding.output_ty != required_binding.output_ty {
426                return Err(format!(
427                    "resource type `{resource_type}` operation `{operation}` has incompatible output type"
428                ));
429            }
430        }
431    }
432    for (name, required_data_type) in required.resources.named_data_types() {
433        let current_data_type = current
434            .resources
435            .resolve_named_data_type(name)
436            .ok_or_else(|| format!("host data type `{name}` is not available"))?;
437        if current_data_type != required_data_type {
438            return Err(format!(
439                "host data type `{name}` has incompatible structure"
440            ));
441        }
442    }
443    for (path, required_binding) in required.resources.value_constructors() {
444        let current_binding = current
445            .resources
446            .resolve_value_constructor(&path.split('.').collect::<Vec<_>>())
447            .ok_or_else(|| format!("value constructor `{path}` is not available"))?;
448        if current_binding.input_ty != required_binding.input_ty {
449            return Err(format!(
450                "value constructor `{path}` has incompatible input type"
451            ));
452        }
453        if current_binding.output_ty != required_binding.output_ty {
454            return Err(format!(
455                "value constructor `{path}` has incompatible output type"
456            ));
457        }
458    }
459    for (source_ty, required_binding) in required.resources.trigger_sources() {
460        let current_binding = current
461            .resources
462            .resolve_trigger_source(source_ty)
463            .ok_or_else(|| format!("trigger source type `{source_ty}` is not available"))?;
464        if current_binding != required_binding {
465            return Err(format!(
466                "trigger source type `{source_ty}` has incompatible event type"
467            ));
468        }
469    }
470
471    Ok(())
472}
473
474#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
475pub struct LashlangProcessInput {
476    pub module_ref: lashlang::ModuleRef,
477    pub process_ref: lashlang::ProcessRef,
478    pub host_requirements_ref: lashlang::HostRequirementsRef,
479    pub process_name: String,
480    #[serde(default)]
481    pub args: serde_json::Map<String, serde_json::Value>,
482}
483
484impl LashlangProcessInput {
485    pub fn process_identity(&self) -> lash_core::ProcessIdentity {
486        lashlang_process_identity(self)
487    }
488
489    pub fn remote_identity(&self) -> lash_remote_protocol::RemoteProcessIdentity {
490        lash_remote_protocol::RemoteProcessIdentity {
491            kind: LASHLANG_ENGINE_KIND.to_string(),
492            label: Some(self.process_name.clone()),
493            definition: Some(lash_remote_protocol::RemoteProcessDefinitionIdentity {
494                value: self.definition(),
495            }),
496        }
497    }
498
499    pub fn to_process_input(&self) -> Result<lash_core::ProcessInput, serde_json::Error> {
500        Ok(lash_core::ProcessInput::Engine {
501            kind: LASHLANG_ENGINE_KIND.to_string(),
502            payload: serde_json::to_value(self)?,
503        })
504    }
505
506    pub fn into_process_input(self) -> Result<lash_core::ProcessInput, serde_json::Error> {
507        self.to_process_input()
508    }
509
510    pub fn remote_trigger_subscription_draft(
511        &self,
512        registrant: lash_remote_protocol::RemoteProcessOriginator,
513        env_ref: lash_remote_protocol::RemoteProcessExecutionEnvRef,
514        source_type: impl Into<String>,
515        source_key: impl Into<String>,
516    ) -> Result<lash_remote_protocol::RemoteTriggerSubscriptionDraft, serde_json::Error> {
517        Ok(
518            lash_remote_protocol::RemoteTriggerSubscriptionDraft::for_process(
519                registrant,
520                env_ref,
521                source_type,
522                source_key,
523                self.clone().try_into()?,
524                self.remote_identity(),
525            ),
526        )
527    }
528
529    pub fn from_payload(payload: serde_json::Value) -> Result<Self, serde_json::Error> {
530        serde_json::from_value(payload)
531    }
532
533    pub fn definition(&self) -> serde_json::Value {
534        serde_json::json!({
535            "module_ref": self.module_ref,
536            "process_ref": self.process_ref,
537            "host_requirements_ref": self.host_requirements_ref,
538            "process_name": self.process_name,
539        })
540    }
541}
542
543impl TryFrom<LashlangProcessInput> for lash_remote_protocol::RemoteProcessInput {
544    type Error = serde_json::Error;
545
546    fn try_from(value: LashlangProcessInput) -> Result<Self, Self::Error> {
547        Ok(Self::Engine {
548            kind: LASHLANG_ENGINE_KIND.to_string(),
549            payload: serde_json::to_value(value)?,
550        })
551    }
552}
553
554#[derive(Clone, Debug)]
555pub struct PreparedLashlangProcessStart {
556    pub registration: lash_core::ProcessRegistration,
557    pub label: Option<String>,
558}
559
560pub async fn prepare_lashlang_process_start(
561    artifact_store: Arc<dyn LashlangArtifactStore>,
562    parent_start_seed: &str,
563    start: lashlang::ProcessStart,
564) -> Result<PreparedLashlangProcessStart, String> {
565    let display_name = Some(start.process_name.clone());
566    let artifact = artifact_store
567        .get_module_artifact(&start.module_ref)
568        .await
569        .map_err(|err| format!("failed to load lashlang module artifact: {err}"))?
570        .ok_or_else(|| {
571            format!(
572                "missing lashlang module artifact `{}` for process `{}`",
573                start.module_ref, start.process_name
574            )
575        })?;
576    if artifact.host_requirements_ref != start.host_requirements_ref {
577        return Err(format!(
578            "lashlang module artifact `{}` host requirements mismatch: process requested {}, artifact has {}",
579            start.module_ref, start.host_requirements_ref, artifact.host_requirements_ref
580        ));
581    }
582    if artifact.process_ref(&start.process_name) != Some(&start.process_ref) {
583        return Err(format!(
584            "lashlang module artifact `{}` does not export process `{}` as requested ref {:?}",
585            start.module_ref, start.process_name, start.process_ref
586        ));
587    }
588    let args = match serde_json::to_value(lashlang::Value::Record(Arc::new(start.args)))
589        .map_err(|err| format!("failed to serialize process args: {err}"))?
590    {
591        serde_json::Value::Object(map) => map,
592        _ => return Err("process args must serialize as a record".to_string()),
593    };
594    let signal_event_types = artifact
595        .canonical_ir
596        .process(&start.process_name)
597        .map(lashlang_process_signal_event_types)
598        .unwrap_or_default();
599    let process_input = LashlangProcessInput {
600        module_ref: start.module_ref,
601        process_ref: start.process_ref,
602        host_requirements_ref: start.host_requirements_ref,
603        process_name: start.process_name,
604        args,
605    };
606    let identity = lashlang_process_identity(&process_input);
607    let process_id =
608        deterministic_lashlang_process_id(parent_start_seed, &start.start_site, &process_input)
609            .map_err(|err| format!("failed to derive deterministic process id: {err}"))?;
610    let process_input = process_input
611        .into_process_input()
612        .map_err(|err| format!("failed to encode process input: {err}"))?;
613    let registration = lash_core::ProcessRegistration::new(
614        process_id,
615        process_input,
616        lash_core::ProcessProvenance::host(),
617    )
618    .with_identity(identity)
619    .with_extra_event_types(
620        lashlang_process_event_types()
621            .into_iter()
622            .chain(signal_event_types),
623    );
624    Ok(PreparedLashlangProcessStart {
625        registration,
626        label: display_name,
627    })
628}
629
630pub fn deterministic_lashlang_process_id(
631    parent_start_seed: &str,
632    start_site: &lashlang::LashlangExecutionCallSite,
633    input: &LashlangProcessInput,
634) -> Result<String, serde_json::Error> {
635    let args = serde_json::to_string(&input.args)?;
636    let occurrence = start_site.occurrence.to_string();
637    let process_ref = lashlang::process_ref_key(&input.process_ref);
638    let mut hasher = Sha256::new();
639    for part in [
640        "lashlang-process-start:v1",
641        parent_start_seed,
642        start_site.site.node_id.as_str(),
643        occurrence.as_str(),
644        input.module_ref.as_str(),
645        process_ref.as_str(),
646        input.host_requirements_ref.as_str(),
647        input.process_name.as_str(),
648        args.as_str(),
649    ] {
650        hasher.update(part.as_bytes());
651        hasher.update([0]);
652    }
653    let hash = format!("{:x}", hasher.finalize());
654    Ok(format!("process:lashlang:sha256:{hash}"))
655}
656
657pub fn resolve_lashlang_module_operation(
658    host_environment: &lashlang::LashlangHostEnvironment,
659    receiver: &lashlang::ResourceHandle,
660    operation: &str,
661) -> Result<String, lashlang::ExecutionHostError> {
662    host_environment
663        .resources
664        .resolve_module_operation(&receiver.resource_type, &receiver.alias, operation)
665        .map(|binding| binding.host_operation.clone())
666        .ok_or_else(|| {
667            lashlang::ExecutionHostError::new(format!(
668                "module `{}` of type `{}` does not expose operation `{operation}`",
669                receiver.alias, receiver.resource_type
670            ))
671        })
672}
673
674fn lashlang_process_identity(input: &LashlangProcessInput) -> lash_core::ProcessIdentity {
675    lash_core::ProcessIdentity::new(LASHLANG_ENGINE_KIND)
676        .with_label(Some(input.process_name.clone()))
677        .with_definition(Some(input.definition()))
678}
679
680#[derive(Clone)]
681pub struct LashlangProcessEngine {
682    artifact_store: Arc<dyn LashlangArtifactStore>,
683    process_cache: Arc<Mutex<CompiledProcessCache>>,
684    surface: LashlangSurface,
685    execution_sink: Option<Arc<dyn lash_trace::TraceSink>>,
686    trace_context: lash_trace::TraceContext,
687}
688
689impl LashlangProcessEngine {
690    pub fn new(artifact_store: Arc<dyn LashlangArtifactStore>, surface: LashlangSurface) -> Self {
691        Self {
692            artifact_store,
693            process_cache: Arc::new(Mutex::new(CompiledProcessCache::new())),
694            surface,
695            execution_sink: None,
696            trace_context: lash_trace::TraceContext::default(),
697        }
698    }
699
700    pub fn in_memory(surface: LashlangSurface) -> Self {
701        Self::new(
702            lashlang::global_in_memory_lashlang_artifact_store(),
703            surface,
704        )
705    }
706
707    pub fn with_execution_trace(
708        mut self,
709        sink: Option<Arc<dyn lash_trace::TraceSink>>,
710        trace_context: lash_trace::TraceContext,
711    ) -> Self {
712        self.execution_sink = sink;
713        self.trace_context = trace_context;
714        self
715    }
716
717    pub fn artifact_store(&self) -> Arc<dyn LashlangArtifactStore> {
718        Arc::clone(&self.artifact_store)
719    }
720}
721
722#[async_trait::async_trait]
723impl lash_core::ProcessEngine for LashlangProcessEngine {
724    fn kind(&self) -> &'static str {
725        LASHLANG_ENGINE_KIND
726    }
727
728    async fn validate_start(
729        &self,
730        context: lash_core::ProcessEngineValidationContext<'_>,
731        payload: &serde_json::Value,
732        _env_spec: Option<&lash_core::ProcessExecutionEnvSpec>,
733    ) -> Result<(), lash_core::PluginError> {
734        let input: LashlangProcessInput =
735            serde_json::from_value(payload.clone()).map_err(|err| {
736                lash_core::PluginError::Session(format!("invalid lashlang process payload: {err}"))
737            })?;
738        let artifact = self
739            .artifact_store
740            .get_module_artifact(&input.module_ref)
741            .await
742            .map_err(|err| lash_core::PluginError::Session(format!("load module artifact: {err}")))?
743            .ok_or_else(|| {
744                lash_core::PluginError::Session(format!(
745                    "missing lashlang module artifact `{}`",
746                    input.module_ref
747                ))
748            })?;
749        if artifact.host_requirements_ref != input.host_requirements_ref {
750            return Err(lash_core::PluginError::Session(format!(
751                "lashlang process `{}` requested surface {}, artifact has {}",
752                input.process_name, input.host_requirements_ref, artifact.host_requirements_ref
753            )));
754        }
755        if artifact.process_ref(&input.process_name) != Some(&input.process_ref) {
756            return Err(lash_core::PluginError::Session(format!(
757                "lashlang module `{}` does not export process `{}` as requested ref {:?}",
758                input.module_ref, input.process_name, input.process_ref
759            )));
760        }
761        let surface = self
762            .surface
763            .clone()
764            .for_process_registry(context.process_registry_available());
765        let host_environment = surface.host_environment(context.tool_catalog());
766        if let Err(err) = lashlang_host_environment_satisfies_requirements(
767            &artifact.host_requirements,
768            &host_environment,
769        ) {
770            return Err(lash_core::PluginError::Session(format!(
771                "lashlang process `{}` is incompatible with this host surface: {err}",
772                input.process_name
773            )));
774        }
775        Ok(())
776    }
777
778    async fn run(
779        &self,
780        context: lash_core::ProcessEngineRunContext<'_>,
781        payload: serde_json::Value,
782    ) -> lash_core::ProcessAwaitOutput {
783        process::run_lashlang_process(self.clone(), context, payload).await
784    }
785
786    fn identity(&self, payload: &serde_json::Value) -> lash_core::ProcessIdentity {
787        match LashlangProcessInput::from_payload(payload.clone()) {
788            Ok(input) => lashlang_process_identity(&input),
789            Err(_) => lash_core::ProcessIdentity::new(LASHLANG_ENGINE_KIND),
790        }
791    }
792}
793
794mod bridge;
795mod process;
796
797pub use bridge::{
798    lashlang_value_to_json, process_event_payload, protocol_tool_output_to_lashlang_value,
799    protocol_tool_reply_to_lashlang_value, sleep_duration_ms,
800};
801pub use process::{
802    lashlang_process_event_types, lashlang_process_signal_event_types, lashlang_type_expr_schema,
803};
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808
809    #[test]
810    fn process_input_serializes_as_generic_engine_payload() {
811        let hash = lashlang::ContentHash::new("abc123");
812        let input = LashlangProcessInput {
813            module_ref: lashlang::ModuleRef::new(&hash),
814            process_ref: lashlang::ProcessRef::new(hash.clone(), 7),
815            host_requirements_ref: lashlang::HostRequirementsRef::new(&hash),
816            process_name: "main".to_string(),
817            args: serde_json::Map::from_iter([("prompt".to_string(), serde_json::json!("go"))]),
818        };
819
820        let process_input = input
821            .clone()
822            .into_process_input()
823            .expect("lashlang process input serializes");
824
825        let lash_core::ProcessInput::Engine { kind, payload } = process_input else {
826            panic!("lashlang runtime must use the generic engine process input");
827        };
828        assert_eq!(kind, LASHLANG_ENGINE_KIND);
829        assert_eq!(
830            LashlangProcessInput::from_payload(payload)
831                .expect("engine payload decodes")
832                .process_name,
833            input.process_name
834        );
835    }
836
837    #[test]
838    fn process_input_remote_helpers_use_generic_engine_and_identity() {
839        let hash = lashlang::ContentHash::new("abc123");
840        let input = LashlangProcessInput {
841            module_ref: lashlang::ModuleRef::new(&hash),
842            process_ref: lashlang::ProcessRef::new(hash.clone(), 7),
843            host_requirements_ref: lashlang::HostRequirementsRef::new(&hash),
844            process_name: "main".to_string(),
845            args: serde_json::Map::from_iter([("prompt".to_string(), serde_json::json!("go"))]),
846        };
847
848        let remote_input: lash_remote_protocol::RemoteProcessInput = input
849            .clone()
850            .try_into()
851            .expect("lashlang process input serializes remotely");
852        let lash_remote_protocol::RemoteProcessInput::Engine { kind, payload } = remote_input
853        else {
854            panic!("lashlang runtime must use the generic remote engine process input");
855        };
856        assert_eq!(kind, LASHLANG_ENGINE_KIND);
857        assert_eq!(
858            LashlangProcessInput::from_payload(payload)
859                .expect("remote payload decodes")
860                .process_name,
861            "main"
862        );
863
864        let identity = input.process_identity();
865        assert_eq!(identity.kind, LASHLANG_ENGINE_KIND);
866        assert_eq!(identity.label.as_deref(), Some("main"));
867        assert_eq!(input.remote_identity().label.as_deref(), Some("main"));
868
869        let draft = input
870            .remote_trigger_subscription_draft(
871                lash_remote_protocol::RemoteProcessOriginator::Host,
872                "process-env:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
873                    .parse()
874                    .expect("canonical env ref"),
875                "ui.button.pressed",
876                "source-key",
877            )
878            .expect("remote trigger draft");
879        draft.validate().expect("draft validates");
880        assert_eq!(draft.target_label.as_deref(), Some("main"));
881        assert_eq!(draft.target_identity.label.as_deref(), Some("main"));
882    }
883
884    #[test]
885    fn tool_binding_defaults_remain_lashlang_local_policy() {
886        let tool = lash_core::ToolDefinition::raw(
887            "tool:test/read_file",
888            "read_file",
889            "read a file",
890            lash_core::ToolDefinition::default_input_schema(),
891            serde_json::Value::Null,
892        );
893
894        let binding = tool_lashlang_binding(&tool.manifest).executable_for(&tool.manifest.name);
895
896        assert_eq!(binding.module_path, vec!["tools"]);
897        assert_eq!(binding.operation, "read.file");
898        assert_eq!(binding.authority_type, "Tools");
899        assert_eq!(binding.call_path(), "tools.read.file");
900    }
901
902    #[test]
903    fn explicit_tool_binding_attaches_lashlang_metadata() {
904        let tool = lash_core::ToolDefinition::raw(
905            "tool:test/read_file",
906            "read_file",
907            "read a file",
908            lash_core::ToolDefinition::default_input_schema(),
909            serde_json::Value::Null,
910        )
911        .with_lashlang_binding(
912            LashlangToolBinding::new(["fs"], "read")
913                .with_authority_type("Filesystem")
914                .with_aliases(["cat"]),
915        );
916
917        let binding = tool_lashlang_binding(&tool.manifest).executable_for(&tool.manifest.name);
918
919        assert_eq!(binding.module_path, vec!["fs"]);
920        assert_eq!(binding.operation, "read");
921        assert_eq!(binding.authority_type, "Filesystem");
922        assert_eq!(binding.aliases, vec!["cat"]);
923    }
924
925    #[test]
926    fn manifest_lashlang_binding_accessor_reports_absent_valid_and_malformed() {
927        let mut manifest = lash_core::ToolDefinition::raw(
928            "tool:test/read_file",
929            "read_file",
930            "read a file",
931            lash_core::ToolDefinition::default_input_schema(),
932            serde_json::Value::Null,
933        )
934        .manifest;
935        assert_eq!(manifest.lashlang_binding().expect("absent binding"), None);
936
937        manifest.bindings.insert(
938            LASHLANG_TOOL_BINDING_KEY.to_string(),
939            serde_json::json!({
940                "module_path": ["fs"],
941                "operation": "read"
942            }),
943        );
944        let binding = manifest
945            .lashlang_binding()
946            .expect("valid binding")
947            .expect("present binding");
948        assert_eq!(binding.module_path, vec!["fs"]);
949        assert_eq!(binding.operation.as_deref(), Some("read"));
950
951        manifest.bindings.insert(
952            LASHLANG_TOOL_BINDING_KEY.to_string(),
953            serde_json::json!({ "module_path": "fs" }),
954        );
955        assert!(manifest.lashlang_binding().is_err());
956    }
957
958    #[test]
959    fn remote_grant_lashlang_binding_accessor_reports_absent_valid_and_malformed() {
960        let grant = remote_tool_grant("read_file");
961        assert_eq!(grant.lashlang_binding().expect("absent binding"), None);
962
963        let grant = grant.with_lashlang_binding(LashlangToolBinding::new(["fs"], "read"));
964        let binding = grant
965            .lashlang_binding()
966            .expect("valid binding")
967            .expect("present binding");
968        assert_eq!(binding.module_path, vec!["fs"]);
969        assert_eq!(binding.operation.as_deref(), Some("read"));
970
971        let mut malformed = grant;
972        malformed.bindings.insert(
973            LASHLANG_TOOL_BINDING_KEY.to_string(),
974            serde_json::json!({ "module_path": "fs" }),
975        );
976        assert!(malformed.lashlang_binding().is_err());
977    }
978
979    #[test]
980    fn deterministic_process_id_reuses_replayed_start_site_and_args() {
981        let input = test_process_input(serde_json::json!({ "root": "." }));
982        let site = test_start_site("child_process:scan", 1);
983
984        let first = deterministic_lashlang_process_id("parent:root", &site, &input)
985            .expect("process id derives");
986        let second = deterministic_lashlang_process_id("parent:root", &site, &input)
987            .expect("process id derives");
988
989        assert_eq!(first, second);
990        assert!(first.starts_with("process:lashlang:sha256:"));
991    }
992
993    #[test]
994    fn deterministic_process_id_separates_parallel_sites_ordinals_and_parents() {
995        let input = test_process_input(serde_json::json!({ "root": "." }));
996        let left = deterministic_lashlang_process_id(
997            "parent:root",
998            &test_start_site("child_process:left", 1),
999            &input,
1000        )
1001        .expect("left id derives");
1002        let right = deterministic_lashlang_process_id(
1003            "parent:root",
1004            &test_start_site("child_process:right", 1),
1005            &input,
1006        )
1007        .expect("right id derives");
1008        let second_ordinal = deterministic_lashlang_process_id(
1009            "parent:root",
1010            &test_start_site("child_process:left", 2),
1011            &input,
1012        )
1013        .expect("second ordinal id derives");
1014        let nested_parent = deterministic_lashlang_process_id(
1015            "parent:nested",
1016            &test_start_site("child_process:left", 1),
1017            &input,
1018        )
1019        .expect("nested parent id derives");
1020
1021        assert_ne!(left, right);
1022        assert_ne!(left, second_ordinal);
1023        assert_ne!(left, nested_parent);
1024    }
1025
1026    #[tokio::test(flavor = "current_thread")]
1027    async fn prepared_start_replays_same_registration_id_without_duplicate_child_identity() {
1028        let store = Arc::new(InMemoryLashlangArtifactStore::new());
1029        let environment = LashlangHostEnvironment::new(
1030            lashlang::LashlangHostCatalog::new(),
1031            LashlangAbilities::default().with_processes(),
1032        );
1033        let output = lashlang::compile_module(lashlang::ModuleCompileRequest {
1034            source: r#"process scan(root: str) -> str { finish root }"#,
1035            environment: &environment,
1036            artifact_store: Some(store.as_ref()),
1037        })
1038        .await
1039        .expect("module compiles and persists");
1040        let artifact_store: Arc<dyn LashlangArtifactStore> = store;
1041        let site = test_start_site("child_process:scan", 1);
1042
1043        let first = prepare_lashlang_process_start(
1044            Arc::clone(&artifact_store),
1045            "parent:root",
1046            test_process_start(&output, site.clone(), "."),
1047        )
1048        .await
1049        .expect("first start prepares");
1050        let replayed = prepare_lashlang_process_start(
1051            Arc::clone(&artifact_store),
1052            "parent:root",
1053            test_process_start(&output, site.clone(), "."),
1054        )
1055        .await
1056        .expect("replayed start prepares");
1057        let sibling = prepare_lashlang_process_start(
1058            Arc::clone(&artifact_store),
1059            "parent:root",
1060            test_process_start(&output, test_start_site("child_process:scan", 2), "."),
1061        )
1062        .await
1063        .expect("sibling start prepares");
1064
1065        assert_eq!(first.registration.id, replayed.registration.id);
1066        assert_eq!(first.registration.identity, replayed.registration.identity);
1067        assert_ne!(first.registration.id, sibling.registration.id);
1068    }
1069
1070    #[test]
1071    fn surface_merges_plugin_extensions() {
1072        let contribution = LashlangSurfaceContribution::new(
1073            LashlangAbilities::default().with_processes(),
1074            LashlangLanguageFeatures::default().with_label_annotations(),
1075            LashlangHostCatalog::tool_default(["lookup"]),
1076        );
1077        let extensions = lash_core::PluginExtensions::from_contributions([
1078            lash_core::PluginExtensionContribution::new(
1079                LASHLANG_SURFACE_EXTENSION_ID,
1080                contribution,
1081            )
1082            .expect("extension payload serializes"),
1083        ]);
1084
1085        let surface = LashlangSurface::default()
1086            .with_plugin_extensions(&extensions)
1087            .expect("lashlang surface extension merges");
1088        let environment = surface.host_environment(&lash_core::ToolCatalog::default());
1089
1090        assert!(environment.abilities.sleep);
1091        assert!(environment.abilities.processes);
1092        assert!(environment.language_features.label_annotations);
1093        assert!(
1094            environment
1095                .resources
1096                .resolve_module_operation("Tools", "tools", "lookup")
1097                .is_some()
1098        );
1099    }
1100
1101    fn remote_tool_grant(name: &str) -> lash_remote_protocol::RemoteToolGrant {
1102        lash_remote_protocol::RemoteToolGrant {
1103            protocol_version: lash_remote_protocol::REMOTE_PROTOCOL_VERSION,
1104            id: format!("remote-tool:{name}"),
1105            name: name.to_string(),
1106            description: String::new(),
1107            input_schema: lash_core::ToolDefinition::default_input_schema(),
1108            output_schema: serde_json::Value::Null,
1109            input_schema_projections: Vec::new(),
1110            output_schema_projections: Vec::new(),
1111            output_contract: lash_remote_protocol::RemoteToolOutputContract::Static,
1112            examples: Vec::new(),
1113            availability: None,
1114            activation: None,
1115            argument_projection: None,
1116            scheduling: None,
1117            retry_policy: None,
1118            bindings: Default::default(),
1119        }
1120    }
1121
1122    fn test_process_input(args: serde_json::Value) -> LashlangProcessInput {
1123        let hash = lashlang::ContentHash::new("abc123");
1124        let args = args
1125            .as_object()
1126            .expect("test args must be an object")
1127            .clone();
1128        LashlangProcessInput {
1129            module_ref: lashlang::ModuleRef::new(&hash),
1130            process_ref: lashlang::ProcessRef::new(hash.clone(), 7),
1131            host_requirements_ref: lashlang::HostRequirementsRef::new(&hash),
1132            process_name: "scan".to_string(),
1133            args,
1134        }
1135    }
1136
1137    fn test_start_site(node_id: &str, occurrence: u64) -> lashlang::LashlangExecutionCallSite {
1138        lashlang::LashlangExecutionCallSite {
1139            site: lashlang::LashlangExecutionSite {
1140                node_id: node_id.to_string(),
1141                node_kind: "child_process".to_string(),
1142                label: "start scan".to_string(),
1143                branch: None,
1144            },
1145            occurrence,
1146        }
1147    }
1148
1149    fn test_process_start(
1150        output: &lashlang::ModuleCompileOutput,
1151        start_site: lashlang::LashlangExecutionCallSite,
1152        root: &str,
1153    ) -> lashlang::ProcessStart {
1154        let mut args = lashlang::Record::new();
1155        args.insert("root".to_string(), lashlang::Value::String(root.into()));
1156        lashlang::ProcessStart {
1157            module_ref: output.module_ref.clone(),
1158            process_ref: output
1159                .artifact
1160                .process_ref("scan")
1161                .expect("scan process export")
1162                .clone(),
1163            host_requirements_ref: output.host_requirements_ref.clone(),
1164            start_site,
1165            process_name: "scan".to_string(),
1166            args,
1167        }
1168    }
1169}