Skip to main content

lash_lashlang_runtime/
lib.rs

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