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