1use std::sync::{Arc, Mutex};
2
3pub use lash_trace::{
4 TraceLashlangChildExecution, TraceLashlangEdgeSelection, TraceLashlangExecutionEvent,
5 TraceLashlangExecutionIdentity, TraceLashlangGraph, TraceLashlangGraphChildLink,
6 TraceLashlangGraphEdge, TraceLashlangGraphNode, TraceLashlangGraphStore, TraceLashlangMap,
7 TraceLashlangMapEdge, TraceLashlangMapNode, TraceLashlangNodeStatus, TraceLashlangStatus,
8};
9pub use lashlang::{
10 CompiledProcessCache, DurabilityTier as LashlangDurabilityTier, InMemoryLashlangArtifactStore,
11 LASH_TYPE_KEY, LashlangAbilities, LashlangArtifactStore, LashlangHostCatalog,
12 LashlangHostEnvironment, LashlangLanguageFeatures,
13};
14
15pub const LASHLANG_ENGINE_KIND: &str = "lashlang";
16pub const LASHLANG_TOOL_BINDING_KEY: &str = "lashlang.tool";
17pub const LASHLANG_SURFACE_EXTENSION_ID: &str = "lashlang.surface";
18
19#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20#[serde(default)]
21pub struct LashlangSurfaceContribution {
22 pub abilities: LashlangAbilities,
23 pub language_features: LashlangLanguageFeatures,
24 pub resources: LashlangHostCatalog,
25}
26
27impl LashlangSurfaceContribution {
28 pub fn new(
29 abilities: LashlangAbilities,
30 language_features: LashlangLanguageFeatures,
31 resources: LashlangHostCatalog,
32 ) -> Self {
33 Self {
34 abilities,
35 language_features,
36 resources,
37 }
38 }
39
40 pub fn from_surface(surface: LashlangSurface) -> Self {
41 Self {
42 abilities: surface.abilities,
43 language_features: surface.language_features,
44 resources: surface.resources,
45 }
46 }
47}
48
49#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50pub struct LashlangToolBinding {
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
52 pub module_path: Vec<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub operation: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub authority_type: Option<String>,
57 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub aliases: Vec<String>,
59}
60
61impl LashlangToolBinding {
62 pub fn new(
63 module_path: impl IntoIterator<Item = impl Into<String>>,
64 operation: impl Into<String>,
65 ) -> Self {
66 Self {
67 module_path: module_path.into_iter().map(Into::into).collect(),
68 operation: Some(operation.into()),
69 authority_type: None,
70 aliases: Vec::new(),
71 }
72 }
73
74 pub fn with_authority_type(mut self, authority_type: impl Into<String>) -> Self {
75 self.authority_type = Some(authority_type.into());
76 self
77 }
78
79 pub fn with_aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
80 self.aliases = aliases.into_iter().map(Into::into).collect();
81 self
82 }
83
84 pub fn executable_for(&self, tool_name: &str) -> ResolvedLashlangToolBinding {
85 let module_path = if self.module_path.is_empty() {
86 vec!["tools".to_string()]
87 } else {
88 self.module_path.clone()
89 };
90 let operation = self
91 .operation
92 .as_deref()
93 .filter(|operation| !operation.trim().is_empty())
94 .map(ToOwned::to_owned)
95 .unwrap_or_else(|| tool_name.replace('_', "."));
96 let authority_type = self
97 .authority_type
98 .as_deref()
99 .filter(|authority_type| !authority_type.trim().is_empty())
100 .map(ToOwned::to_owned)
101 .unwrap_or_else(|| default_authority_type(&module_path));
102 ResolvedLashlangToolBinding {
103 module_path,
104 operation,
105 authority_type,
106 aliases: self.aliases.clone(),
107 }
108 }
109
110 pub fn required_for_remote(
111 manifest: &lash_core::ToolManifest,
112 ) -> Result<ResolvedLashlangToolBinding, String> {
113 tool_lashlang_binding(manifest).required_executable_for_remote(&manifest.name)
114 }
115
116 pub fn required_executable_for_remote(
117 &self,
118 tool_name: &str,
119 ) -> Result<ResolvedLashlangToolBinding, String> {
120 if self.module_path.is_empty() {
121 return Err(format!(
122 "tool `{tool_name}` is missing an explicit remote module path"
123 ));
124 }
125 let operation = self
126 .operation
127 .as_deref()
128 .filter(|operation| !operation.trim().is_empty())
129 .ok_or_else(|| {
130 format!("tool `{tool_name}` is missing an explicit remote operation name")
131 })?;
132 let authority_type = self
133 .authority_type
134 .as_deref()
135 .filter(|authority_type| !authority_type.trim().is_empty())
136 .map(ToOwned::to_owned)
137 .unwrap_or_else(|| default_authority_type(&self.module_path));
138 Ok(ResolvedLashlangToolBinding {
139 module_path: self.module_path.clone(),
140 operation: operation.to_string(),
141 authority_type,
142 aliases: self.aliases.clone(),
143 })
144 }
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
148pub struct ResolvedLashlangToolBinding {
149 pub module_path: Vec<String>,
150 pub operation: String,
151 pub authority_type: String,
152 pub aliases: Vec<String>,
153}
154
155impl ResolvedLashlangToolBinding {
156 pub fn module_path_string(&self) -> String {
157 self.module_path.join(".")
158 }
159
160 pub fn call_path(&self) -> String {
161 format!("{}.{}", self.module_path_string(), self.operation)
162 }
163}
164
165fn default_authority_type(module_path: &[String]) -> String {
166 module_path
167 .last()
168 .map(|segment| {
169 let mut chars = segment.chars();
170 match chars.next() {
171 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
172 None => "Tool".to_string(),
173 }
174 })
175 .unwrap_or_else(|| "Tool".to_string())
176}
177
178pub fn tool_lashlang_binding(manifest: &lash_core::ToolManifest) -> LashlangToolBinding {
179 manifest
180 .bindings
181 .get(LASHLANG_TOOL_BINDING_KEY)
182 .cloned()
183 .and_then(|value| serde_json::from_value(value).ok())
184 .unwrap_or_default()
185}
186
187pub trait ToolDefinitionLashlangExt {
188 fn with_lashlang_binding(self, lashlang_binding: LashlangToolBinding) -> Self;
189}
190
191impl ToolDefinitionLashlangExt for lash_core::ToolDefinition {
192 fn with_lashlang_binding(mut self, lashlang_binding: LashlangToolBinding) -> Self {
193 let value = serde_json::to_value(lashlang_binding)
194 .expect("lashlang tool binding must serialize to JSON");
195 self.manifest
196 .bindings
197 .insert(LASHLANG_TOOL_BINDING_KEY.to_string(), value);
198 self
199 }
200}
201
202#[derive(Clone, Debug)]
203pub struct LashlangSurface {
204 pub abilities: LashlangAbilities,
205 pub language_features: LashlangLanguageFeatures,
206 pub resources: LashlangHostCatalog,
207}
208
209impl Default for LashlangSurface {
210 fn default() -> Self {
211 Self {
212 abilities: LashlangAbilities::default().with_sleep(),
213 language_features: LashlangLanguageFeatures::default(),
214 resources: LashlangHostCatalog::new(),
215 }
216 }
217}
218
219impl LashlangSurface {
220 pub fn new(
221 abilities: LashlangAbilities,
222 language_features: LashlangLanguageFeatures,
223 resources: LashlangHostCatalog,
224 ) -> Self {
225 Self {
226 abilities,
227 language_features,
228 resources,
229 }
230 }
231
232 pub fn for_process_registry(mut self, process_registry_available: bool) -> Self {
233 self.abilities = self.abilities.with_sleep();
234 if process_registry_available {
235 self.abilities = self.abilities.with_processes().with_process_signals();
236 } else {
237 self.abilities.processes = false;
238 self.abilities.process_signals = false;
239 }
240 self
241 }
242
243 pub fn with_resources(mut self, resources: LashlangHostCatalog) -> Self {
244 self.resources.extend(resources);
245 self
246 }
247
248 pub fn with_plugin_extensions(
249 mut self,
250 extensions: &lash_core::PluginExtensions,
251 ) -> Result<Self, String> {
252 for payload in extensions.payloads(LASHLANG_SURFACE_EXTENSION_ID) {
253 let contribution: LashlangSurfaceContribution = serde_json::from_value(payload.clone())
254 .map_err(|err| {
255 format!("invalid `{LASHLANG_SURFACE_EXTENSION_ID}` extension payload: {err}")
256 })?;
257 self.abilities = self.abilities.union(contribution.abilities);
258 self.language_features = self.language_features.union(contribution.language_features);
259 self.resources.extend(contribution.resources);
260 }
261 Ok(self)
262 }
263
264 pub fn host_environment(&self, catalog: &lash_core::ToolCatalog) -> LashlangHostEnvironment {
265 lashlang_host_environment_from_tool_catalog(
266 catalog,
267 self.abilities,
268 self.language_features,
269 self.resources.clone(),
270 )
271 }
272}
273
274pub fn lashlang_host_environment_from_tool_catalog(
275 catalog: &lash_core::ToolCatalog,
276 abilities: LashlangAbilities,
277 language_features: LashlangLanguageFeatures,
278 host_resources: LashlangHostCatalog,
279) -> LashlangHostEnvironment {
280 let mut resources = lashlang_resources_from_tool_catalog(catalog);
281 resources.extend(host_resources);
282 if abilities.triggers {
283 lashlang::add_trigger_resource_operations(&mut resources);
284 }
285 LashlangHostEnvironment::new(resources, abilities).with_language_features(language_features)
286}
287
288pub fn lashlang_resources_from_tool_catalog(
289 catalog: &lash_core::ToolCatalog,
290) -> LashlangHostCatalog {
291 let mut host_catalog = LashlangHostCatalog::new();
292 for entry in catalog.tools.iter() {
293 if entry.availability.is_callable() {
294 let lashlang_binding =
295 tool_lashlang_binding(&entry.manifest).executable_for(&entry.manifest.name);
296 host_catalog.add_module_operation(
297 lashlang_binding.module_path.iter().map(String::as_str),
298 lashlang_binding.authority_type.clone(),
299 lashlang_binding.operation.clone(),
300 entry.manifest.name.clone(),
301 lashlang::TypeExpr::Any,
302 lashlang::TypeExpr::Any,
303 );
304 }
305 }
306 host_catalog
307}
308
309pub fn lashlang_host_environment_satisfies_requirements(
310 required: &lashlang::HostRequirements,
311 current: &LashlangHostEnvironment,
312) -> Result<(), String> {
313 let abilities = required.abilities;
314 let current_abilities = current.abilities;
315 if abilities.processes && !current_abilities.processes {
316 return Err("processes are not available".to_string());
317 }
318 if abilities.sleep && !current_abilities.sleep {
319 return Err("sleep is not available".to_string());
320 }
321 if abilities.process_signals && !current_abilities.process_signals {
322 return Err("process signals are not available".to_string());
323 }
324 if abilities.triggers && !current_abilities.triggers {
325 return Err("triggers are not available".to_string());
326 }
327 if required.language_features.label_annotations && !current.language_features.label_annotations
328 {
329 return Err("label annotations are not available".to_string());
330 }
331
332 for (_, module) in required.resources.module_instances() {
333 let current_module = current
334 .resources
335 .resolve_module_path(&module.path)
336 .ok_or_else(|| format!("module `{}` is not available", module.alias))?;
337 if current_module.resource_type != module.resource_type {
338 return Err(format!(
339 "module `{}` has type `{}`, expected `{}`",
340 module.alias, current_module.resource_type, module.resource_type
341 ));
342 }
343 for (operation, required_binding) in &module.operations {
344 match current.resources.resolve_module_operation(
345 &module.resource_type,
346 &module.alias,
347 operation,
348 ) {
349 Some(current_binding) if current_binding == required_binding => {}
350 Some(current_binding) => {
351 return Err(format!(
352 "module `{}` operation `{operation}` resolves to `{}`, expected `{}`",
353 module.alias,
354 current_binding.host_operation,
355 required_binding.host_operation
356 ));
357 }
358 None => {
359 return Err(format!(
360 "module `{}` does not expose operation `{operation}`",
361 module.alias
362 ));
363 }
364 }
365 }
366 }
367
368 for (resource_type, required_type) in required.resources.resource_types() {
369 if !current.resources.has_resource_type(resource_type) {
370 return Err(format!("resource type `{resource_type}` is not available"));
371 }
372 for (operation, required_binding) in &required_type.operations {
373 let current_binding = current
374 .resources
375 .resolve_operation(resource_type, operation)
376 .ok_or_else(|| {
377 format!(
378 "resource type `{resource_type}` does not expose operation `{operation}`"
379 )
380 })?;
381 if current_binding.input_ty != required_binding.input_ty {
382 return Err(format!(
383 "resource type `{resource_type}` operation `{operation}` has incompatible input type"
384 ));
385 }
386 if current_binding.output_ty != required_binding.output_ty {
387 return Err(format!(
388 "resource type `{resource_type}` operation `{operation}` has incompatible output type"
389 ));
390 }
391 }
392 }
393 for (name, required_data_type) in required.resources.named_data_types() {
394 let current_data_type = current
395 .resources
396 .resolve_named_data_type(name)
397 .ok_or_else(|| format!("host data type `{name}` is not available"))?;
398 if current_data_type != required_data_type {
399 return Err(format!(
400 "host data type `{name}` has incompatible structure"
401 ));
402 }
403 }
404 for (path, required_binding) in required.resources.value_constructors() {
405 let current_binding = current
406 .resources
407 .resolve_value_constructor(&path.split('.').collect::<Vec<_>>())
408 .ok_or_else(|| format!("value constructor `{path}` is not available"))?;
409 if current_binding.input_ty != required_binding.input_ty {
410 return Err(format!(
411 "value constructor `{path}` has incompatible input type"
412 ));
413 }
414 if current_binding.output_ty != required_binding.output_ty {
415 return Err(format!(
416 "value constructor `{path}` has incompatible output type"
417 ));
418 }
419 }
420 for (source_ty, required_binding) in required.resources.trigger_sources() {
421 let current_binding = current
422 .resources
423 .resolve_trigger_source(source_ty)
424 .ok_or_else(|| format!("trigger source type `{source_ty}` is not available"))?;
425 if current_binding != required_binding {
426 return Err(format!(
427 "trigger source type `{source_ty}` has incompatible event type"
428 ));
429 }
430 }
431
432 Ok(())
433}
434
435#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
436pub struct LashlangProcessInput {
437 pub module_ref: lashlang::ModuleRef,
438 pub process_ref: lashlang::ProcessRef,
439 pub host_requirements_ref: lashlang::HostRequirementsRef,
440 pub process_name: String,
441 #[serde(default)]
442 pub args: serde_json::Map<String, serde_json::Value>,
443}
444
445impl LashlangProcessInput {
446 pub fn into_process_input(self) -> Result<lash_core::ProcessInput, serde_json::Error> {
447 Ok(lash_core::ProcessInput::Engine {
448 kind: LASHLANG_ENGINE_KIND.to_string(),
449 payload: serde_json::to_value(self)?,
450 })
451 }
452
453 pub fn from_payload(payload: serde_json::Value) -> Result<Self, serde_json::Error> {
454 serde_json::from_value(payload)
455 }
456
457 pub fn definition(&self) -> serde_json::Value {
458 serde_json::json!({
459 "module_ref": self.module_ref,
460 "process_ref": self.process_ref,
461 "host_requirements_ref": self.host_requirements_ref,
462 "process_name": self.process_name,
463 })
464 }
465}
466
467#[derive(Clone, Debug)]
468pub struct PreparedLashlangProcessStart {
469 pub registration: lash_core::ProcessRegistration,
470 pub label: Option<String>,
471}
472
473pub async fn prepare_lashlang_process_start(
474 artifact_store: Arc<dyn LashlangArtifactStore>,
475 start: lashlang::ProcessStart,
476) -> Result<PreparedLashlangProcessStart, String> {
477 let display_name = Some(start.process_name.clone());
478 let artifact = artifact_store
479 .get_module_artifact(&start.module_ref)
480 .await
481 .map_err(|err| format!("failed to load lashlang module artifact: {err}"))?
482 .ok_or_else(|| {
483 format!(
484 "missing lashlang module artifact `{}` for process `{}`",
485 start.module_ref, start.process_name
486 )
487 })?;
488 if artifact.host_requirements_ref != start.host_requirements_ref {
489 return Err(format!(
490 "lashlang module artifact `{}` host requirements mismatch: process requested {}, artifact has {}",
491 start.module_ref, start.host_requirements_ref, artifact.host_requirements_ref
492 ));
493 }
494 if artifact.process_ref(&start.process_name) != Some(&start.process_ref) {
495 return Err(format!(
496 "lashlang module artifact `{}` does not export process `{}` as requested ref {:?}",
497 start.module_ref, start.process_name, start.process_ref
498 ));
499 }
500 let args = match serde_json::to_value(lashlang::Value::Record(Arc::new(start.args)))
501 .map_err(|err| format!("failed to serialize process args: {err}"))?
502 {
503 serde_json::Value::Object(map) => map,
504 _ => return Err("process args must serialize as a record".to_string()),
505 };
506 let signal_event_types = artifact
507 .canonical_ir
508 .process(&start.process_name)
509 .map(lashlang_process_signal_event_types)
510 .unwrap_or_default();
511 let process_input = LashlangProcessInput {
512 module_ref: start.module_ref,
513 process_ref: start.process_ref,
514 host_requirements_ref: start.host_requirements_ref,
515 process_name: start.process_name,
516 args,
517 };
518 let identity = lashlang_process_identity(&process_input);
519 let process_input = process_input
520 .into_process_input()
521 .map_err(|err| format!("failed to encode process input: {err}"))?;
522 let process_id = format!("process:{}", uuid::Uuid::new_v4());
523 let registration = lash_core::ProcessRegistration::new(
524 process_id,
525 process_input,
526 lash_core::ProcessProvenance::host(),
527 )
528 .with_identity(identity)
529 .with_extra_event_types(
530 lashlang_process_event_types()
531 .into_iter()
532 .chain(signal_event_types),
533 );
534 Ok(PreparedLashlangProcessStart {
535 registration,
536 label: display_name,
537 })
538}
539
540pub fn resolve_lashlang_module_operation(
541 host_environment: &lashlang::LashlangHostEnvironment,
542 receiver: &lashlang::ResourceHandle,
543 operation: &str,
544) -> Result<String, lashlang::ExecutionHostError> {
545 host_environment
546 .resources
547 .resolve_module_operation(&receiver.resource_type, &receiver.alias, operation)
548 .map(|binding| binding.host_operation.clone())
549 .ok_or_else(|| {
550 lashlang::ExecutionHostError::new(format!(
551 "module `{}` of type `{}` does not expose operation `{operation}`",
552 receiver.alias, receiver.resource_type
553 ))
554 })
555}
556
557fn lashlang_process_identity(input: &LashlangProcessInput) -> lash_core::ProcessIdentity {
558 lash_core::ProcessIdentity::new(LASHLANG_ENGINE_KIND)
559 .with_label(Some(input.process_name.clone()))
560 .with_definition(Some(input.definition()))
561}
562
563#[derive(Clone)]
564pub struct LashlangProcessEngine {
565 artifact_store: Arc<dyn LashlangArtifactStore>,
566 process_cache: Arc<Mutex<CompiledProcessCache>>,
567 surface: LashlangSurface,
568 execution_sink: Option<Arc<dyn lash_trace::TraceSink>>,
569 trace_context: lash_trace::TraceContext,
570}
571
572impl LashlangProcessEngine {
573 pub fn new(artifact_store: Arc<dyn LashlangArtifactStore>, surface: LashlangSurface) -> Self {
574 Self {
575 artifact_store,
576 process_cache: Arc::new(Mutex::new(CompiledProcessCache::new())),
577 surface,
578 execution_sink: None,
579 trace_context: lash_trace::TraceContext::default(),
580 }
581 }
582
583 pub fn in_memory(surface: LashlangSurface) -> Self {
584 Self::new(
585 lashlang::global_in_memory_lashlang_artifact_store(),
586 surface,
587 )
588 }
589
590 pub fn with_execution_trace(
591 mut self,
592 sink: Option<Arc<dyn lash_trace::TraceSink>>,
593 trace_context: lash_trace::TraceContext,
594 ) -> Self {
595 self.execution_sink = sink;
596 self.trace_context = trace_context;
597 self
598 }
599
600 pub fn artifact_store(&self) -> Arc<dyn LashlangArtifactStore> {
601 Arc::clone(&self.artifact_store)
602 }
603}
604
605#[async_trait::async_trait]
606impl lash_core::ProcessEngine for LashlangProcessEngine {
607 fn kind(&self) -> &'static str {
608 LASHLANG_ENGINE_KIND
609 }
610
611 async fn validate_start(
612 &self,
613 context: lash_core::ProcessEngineValidationContext<'_>,
614 payload: &serde_json::Value,
615 _env_spec: Option<&lash_core::ProcessExecutionEnvSpec>,
616 ) -> Result<(), lash_core::PluginError> {
617 let input: LashlangProcessInput =
618 serde_json::from_value(payload.clone()).map_err(|err| {
619 lash_core::PluginError::Session(format!("invalid lashlang process payload: {err}"))
620 })?;
621 let artifact = self
622 .artifact_store
623 .get_module_artifact(&input.module_ref)
624 .await
625 .map_err(|err| lash_core::PluginError::Session(format!("load module artifact: {err}")))?
626 .ok_or_else(|| {
627 lash_core::PluginError::Session(format!(
628 "missing lashlang module artifact `{}`",
629 input.module_ref
630 ))
631 })?;
632 if artifact.host_requirements_ref != input.host_requirements_ref {
633 return Err(lash_core::PluginError::Session(format!(
634 "lashlang process `{}` requested surface {}, artifact has {}",
635 input.process_name, input.host_requirements_ref, artifact.host_requirements_ref
636 )));
637 }
638 if artifact.process_ref(&input.process_name) != Some(&input.process_ref) {
639 return Err(lash_core::PluginError::Session(format!(
640 "lashlang module `{}` does not export process `{}` as requested ref {:?}",
641 input.module_ref, input.process_name, input.process_ref
642 )));
643 }
644 let surface = self
645 .surface
646 .clone()
647 .for_process_registry(context.process_registry_available());
648 let host_environment = surface.host_environment(context.tool_catalog());
649 if let Err(err) = lashlang_host_environment_satisfies_requirements(
650 &artifact.host_requirements,
651 &host_environment,
652 ) {
653 return Err(lash_core::PluginError::Session(format!(
654 "lashlang process `{}` is incompatible with this host surface: {err}",
655 input.process_name
656 )));
657 }
658 Ok(())
659 }
660
661 async fn run(
662 &self,
663 context: lash_core::ProcessEngineRunContext<'_>,
664 payload: serde_json::Value,
665 ) -> lash_core::ProcessAwaitOutput {
666 process::run_lashlang_process(self.clone(), context, payload).await
667 }
668
669 fn identity(&self, payload: &serde_json::Value) -> lash_core::ProcessIdentity {
670 match LashlangProcessInput::from_payload(payload.clone()) {
671 Ok(input) => lashlang_process_identity(&input),
672 Err(_) => lash_core::ProcessIdentity::new(LASHLANG_ENGINE_KIND),
673 }
674 }
675}
676
677mod bridge;
678mod process;
679
680pub use bridge::{
681 lashlang_value_to_json, process_event_payload, protocol_tool_output_to_lashlang_value,
682 protocol_tool_reply_to_lashlang_value, sleep_duration_ms,
683};
684pub use process::{
685 lashlang_process_event_types, lashlang_process_signal_event_types, lashlang_type_expr_schema,
686};
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn process_input_serializes_as_generic_engine_payload() {
694 let hash = lashlang::ContentHash::new("abc123");
695 let input = LashlangProcessInput {
696 module_ref: lashlang::ModuleRef::new(&hash),
697 process_ref: lashlang::ProcessRef::new(hash.clone(), 7),
698 host_requirements_ref: lashlang::HostRequirementsRef::new(&hash),
699 process_name: "main".to_string(),
700 args: serde_json::Map::from_iter([("prompt".to_string(), serde_json::json!("go"))]),
701 };
702
703 let process_input = input
704 .clone()
705 .into_process_input()
706 .expect("lashlang process input serializes");
707
708 let lash_core::ProcessInput::Engine { kind, payload } = process_input else {
709 panic!("lashlang runtime must use the generic engine process input");
710 };
711 assert_eq!(kind, LASHLANG_ENGINE_KIND);
712 assert_eq!(
713 LashlangProcessInput::from_payload(payload)
714 .expect("engine payload decodes")
715 .process_name,
716 input.process_name
717 );
718 }
719
720 #[test]
721 fn tool_binding_defaults_remain_lashlang_local_policy() {
722 let tool = lash_core::ToolDefinition::raw_named(
723 "read_file",
724 "read a file",
725 lash_core::ToolDefinition::default_input_schema(),
726 serde_json::Value::Null,
727 );
728
729 let binding = tool_lashlang_binding(&tool.manifest).executable_for(&tool.manifest.name);
730
731 assert_eq!(binding.module_path, vec!["tools"]);
732 assert_eq!(binding.operation, "read.file");
733 assert_eq!(binding.authority_type, "Tools");
734 assert_eq!(binding.call_path(), "tools.read.file");
735 }
736
737 #[test]
738 fn explicit_tool_binding_attaches_lashlang_metadata() {
739 let tool = lash_core::ToolDefinition::raw_named(
740 "read_file",
741 "read a file",
742 lash_core::ToolDefinition::default_input_schema(),
743 serde_json::Value::Null,
744 )
745 .with_lashlang_binding(
746 LashlangToolBinding::new(["fs"], "read")
747 .with_authority_type("Filesystem")
748 .with_aliases(["cat"]),
749 );
750
751 let binding = tool_lashlang_binding(&tool.manifest).executable_for(&tool.manifest.name);
752
753 assert_eq!(binding.module_path, vec!["fs"]);
754 assert_eq!(binding.operation, "read");
755 assert_eq!(binding.authority_type, "Filesystem");
756 assert_eq!(binding.aliases, vec!["cat"]);
757 }
758
759 #[test]
760 fn surface_merges_plugin_extensions() {
761 let contribution = LashlangSurfaceContribution::new(
762 LashlangAbilities::default().with_processes(),
763 LashlangLanguageFeatures::default().with_label_annotations(),
764 LashlangHostCatalog::tool_default(["lookup"]),
765 );
766 let extensions = lash_core::PluginExtensions::from_contributions([
767 lash_core::PluginExtensionContribution::new(
768 LASHLANG_SURFACE_EXTENSION_ID,
769 contribution,
770 )
771 .expect("extension payload serializes"),
772 ]);
773
774 let surface = LashlangSurface::default()
775 .with_plugin_extensions(&extensions)
776 .expect("lashlang surface extension merges");
777 let environment = surface.host_environment(&lash_core::ToolCatalog::default());
778
779 assert!(environment.abilities.sleep);
780 assert!(environment.abilities.processes);
781 assert!(environment.language_features.label_annotations);
782 assert!(
783 environment
784 .resources
785 .resolve_module_operation("Tools", "tools", "lookup")
786 .is_some()
787 );
788 }
789}