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}