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