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