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