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