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 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}