1use super::errors::PackageError;
2use super::*;
3
4pub(crate) fn manifest_capabilities(
5 manifest: &Manifest,
6) -> Option<&harn_vm::llm::capabilities::CapabilitiesFile> {
7 manifest.capabilities.as_ref()
8}
9
10pub(crate) fn is_empty_capabilities(file: &harn_vm::llm::capabilities::CapabilitiesFile) -> bool {
11 file.provider.is_empty() && file.provider_family.is_empty()
12}
13
14pub fn validate_runtime_manifest_extensions(anchor: &Path) -> Result<(), PackageError> {
15 let Some((manifest, _manifest_dir)) = load_nearest_manifest(anchor).into_result()? else {
16 return Ok(());
17 };
18 validate_handoff_routes(&manifest.handoff_routes, &manifest)?;
19 validate_contributions(&manifest)
20}
21
22pub fn try_load_runtime_extensions(anchor: &Path) -> Result<RuntimeExtensions, PackageError> {
25 ensure_dependencies_materialized(anchor)?;
26 let Some((root_manifest, manifest_dir)) = load_nearest_manifest(anchor).into_result()? else {
27 return Ok(RuntimeExtensions::default());
28 };
29
30 let mut llm = harn_vm::llm_config::ProvidersConfig::default();
31 let mut capabilities = harn_vm::llm::capabilities::CapabilitiesFile::default();
32 let mut hooks = Vec::new();
33 let mut triggers = Vec::new();
34
35 llm.merge_from(&root_manifest.llm);
36 if let Some(file) = manifest_capabilities(&root_manifest) {
37 merge_capability_overrides(&mut capabilities, file);
38 }
39 hooks.extend(resolved_hooks_from_manifest(&root_manifest, &manifest_dir));
40 triggers.extend(resolved_triggers_from_manifest(
41 &root_manifest,
42 &manifest_dir,
43 ));
44 let handoff_routes = root_manifest.handoff_routes.clone();
45 validate_handoff_routes(&handoff_routes, &root_manifest)?;
46 let mut provider_connectors =
47 resolved_provider_connectors_from_manifest(&root_manifest, &manifest_dir);
48 let package_snapshot =
49 dependency_package_snapshot(&root_manifest, &manifest_dir)?.map(Arc::new);
50 if let Some(snapshot) = package_snapshot.as_ref() {
51 provider_connectors.extend(installed_package_provider_connectors(
52 snapshot,
53 snapshot.packages_root(),
54 )?);
55 }
56 provider_connectors = dedupe_provider_connectors(provider_connectors);
57 let root_manifest_path = manifest_dir.join(MANIFEST);
58 let runtime_personas = resolve_runtime_personas(
59 root_manifest.clone(),
60 root_manifest_path.clone(),
61 manifest_dir.clone(),
62 package_snapshot,
63 )?;
64 triggers.extend(installed_persona_trigger_configs(&runtime_personas)?);
65
66 Ok(RuntimeExtensions {
67 root_manifest_path: Some(root_manifest_path),
68 root_manifest_dir: Some(manifest_dir),
69 root_manifest: Some(root_manifest),
70 runtime_personas,
71 llm: (!llm.is_empty()).then_some(llm),
72 capabilities: (!is_empty_capabilities(&capabilities)).then_some(capabilities),
73 hooks,
74 triggers,
75 handoff_routes,
76 provider_connectors,
77 })
78}
79
80pub fn try_load_runtime_extensions_from_manifest(
85 manifest_path: &Path,
86) -> Result<Option<RuntimeExtensions>, PackageError> {
87 let manifest_path = if manifest_path.is_dir() {
88 manifest_path.join(MANIFEST)
89 } else {
90 manifest_path.to_path_buf()
91 };
92 if manifest_path.extension().and_then(|value| value.to_str()) == Some("harn") {
93 return Ok(None);
94 }
95 if manifest_path.file_name() != Some(OsStr::new(MANIFEST)) {
96 return Ok(None);
97 }
98 if read_manifest_from_path(&manifest_path).is_err() {
99 return Ok(None);
100 }
101 try_load_runtime_extensions(&manifest_path).map(Some)
102}
103
104fn installed_package_provider_connectors(
105 snapshot: &harn_modules::package_snapshot::PackageSnapshot,
106 packages_dir: &Path,
107) -> Result<Vec<ResolvedProviderConnectorConfig>, PackageError> {
108 let lock = LockFile::load(snapshot.lock_path())?.ok_or_else(|| {
109 PackageError::Lockfile(format!(
110 "published package generation is missing {}",
111 snapshot.lock_path().display()
112 ))
113 })?;
114 let mut providers = Vec::new();
115 for entry in &lock.packages {
116 validate_package_alias(&entry.name)?;
117 let package_dir = packages_dir.join(&entry.name);
118 if package_dir.is_dir() {
119 if let Some(manifest) = read_package_manifest_from_dir(&package_dir)? {
120 providers.extend(resolved_provider_connectors_from_manifest(
121 &manifest,
122 &package_dir,
123 ));
124 }
125 continue;
126 }
127
128 let package_file = packages_dir.join(format!("{}.harn", entry.name));
129 if package_file.is_file() {
130 continue;
131 }
132
133 return Err(PackageError::Manifest(format!(
134 "installed package {} is missing under {}; run `harn install`",
135 entry.name,
136 packages_dir.display()
137 )));
138 }
139 Ok(providers)
140}
141
142fn dedupe_provider_connectors(
143 providers: Vec<ResolvedProviderConnectorConfig>,
144) -> Vec<ResolvedProviderConnectorConfig> {
145 let mut seen = std::collections::BTreeSet::new();
146 let mut out = Vec::new();
147 for provider in providers {
148 if seen.insert(provider.id.as_str().to_string()) {
149 out.push(provider);
150 }
151 }
152 out
153}
154
155pub async fn load_provider_connector(
158 config: &ResolvedProviderConnectorConfig,
159) -> Result<Option<Box<dyn harn_vm::Connector>>, PackageError> {
160 match &config.connector {
161 ResolvedProviderConnectorKind::RustBuiltin => Ok(None),
162 ResolvedProviderConnectorKind::Invalid(message) => {
163 Err(PackageError::Validation(message.clone()))
164 }
165 ResolvedProviderConnectorKind::Harn { module } => {
166 let module_path = harn_vm::resolve_module_import_path(&config.manifest_dir, module);
167 let connector = harn_vm::HarnConnector::load(&module_path)
168 .await
169 .map_err(|error| {
170 PackageError::Validation(format!(
171 "failed to load Harn connector '{}' for provider '{}': {error}",
172 module_path.display(),
173 config.id.as_str()
174 ))
175 })?;
176 let observed = harn_vm::Connector::provider_id(&connector);
177 if observed != &config.id {
178 return Err(PackageError::Validation(format!(
179 "provider '{}' resolves to connector module '{}' which declares provider_id '{}'",
180 config.id.as_str(),
181 module_path.display(),
182 observed.as_str()
183 )));
184 }
185 Ok(Some(Box::new(connector)))
186 }
187 }
188}
189
190pub fn load_runtime_extensions(anchor: &Path) -> RuntimeExtensions {
191 match try_load_runtime_extensions(anchor) {
192 Ok(extensions) => extensions,
193 Err(error) => {
194 eprintln!("error: {error}");
195 process::exit(1);
196 }
197 }
198}
199
200pub fn install_runtime_extensions(extensions: &RuntimeExtensions) {
202 harn_vm::llm_config::set_user_overrides(extensions.llm.clone());
203 harn_vm::llm::capabilities::set_user_overrides(extensions.capabilities.clone());
204 install_manifest_handoff_routes(extensions);
205 install_orchestrator_budget(extensions);
206}
207
208pub fn install_manifest_handoff_routes(extensions: &RuntimeExtensions) {
209 harn_vm::install_handoff_routes(extensions.handoff_routes.clone());
210}
211
212pub fn install_orchestrator_budget(extensions: &RuntimeExtensions) {
213 let budget = extensions
214 .root_manifest
215 .as_ref()
216 .map(|manifest| harn_vm::OrchestratorBudgetConfig {
217 daily_cost_usd: manifest.orchestrator.budget.daily_cost_usd,
218 hourly_cost_usd: manifest.orchestrator.budget.hourly_cost_usd,
219 })
220 .unwrap_or_default();
221 harn_vm::install_orchestrator_budget(budget);
222}
223
224pub async fn install_manifest_hooks(
225 vm: &mut harn_vm::Vm,
226 extensions: &RuntimeExtensions,
227) -> Result<(), PackageError> {
228 install_manifest_hooks_with_mode(vm, extensions, false).await
229}
230
231pub async fn install_manifest_hooks_with_mode(
239 vm: &mut harn_vm::Vm,
240 extensions: &RuntimeExtensions,
241 lazy: bool,
242) -> Result<(), PackageError> {
243 harn_vm::orchestration::clear_runtime_hooks();
244 let mut loaded_exports: HashMap<ManifestModuleCacheKey, ManifestModuleExports> = HashMap::new();
245 let mut module_signatures: HashMap<PathBuf, Vec<CachedModuleCallableSignatures>> =
246 HashMap::new();
247 for hook in &extensions.hooks {
248 let Some((module_name, function_name)) = hook.handler.rsplit_once("::") else {
249 return Err(format!(
250 "invalid hook handler '{}': expected <module>::<function>",
251 hook.handler
252 )
253 .into());
254 };
255 let module_path = crate::package::manifest_module_source_path(
256 &hook.manifest_dir,
257 hook.package_name.as_deref(),
258 &hook.exports,
259 Some(module_name),
260 )?;
261 let signatures =
262 cached_module_callable_signatures(&mut module_signatures, &module_path, None)?;
263 if signatures
264 .get(function_name)
265 .is_none_or(|signature| !signature.is_pub)
266 {
267 return Err(format!(
268 "hook handler '{function_name}' is not exported by module '{module_name}'"
269 )
270 .into());
271 }
272 if lazy {
273 harn_vm::orchestration::register_vm_hook_lazy(
274 hook.event,
275 hook.pattern.clone(),
276 hook.handler.clone(),
277 harn_vm::LazyVmCallable::new(module_path, function_name),
278 );
279 continue;
280 }
281 let cache_key = (
282 hook.manifest_dir.clone(),
283 hook.package_name.clone(),
284 Some(module_name.to_string()),
285 );
286 if !loaded_exports.contains_key(&cache_key) {
287 let exports = resolve_manifest_exports(
288 vm,
289 &hook.manifest_dir,
290 hook.package_name.as_deref(),
291 &hook.exports,
292 Some(module_name),
293 )
294 .await?;
295 loaded_exports.insert(cache_key.clone(), exports);
296 }
297 let exports = loaded_exports
298 .get(&cache_key)
299 .expect("manifest hook exports cached");
300 let Some(closure) = exports.get(function_name) else {
301 return Err(format!(
302 "hook handler '{function_name}' is not exported by module '{module_name}'"
303 )
304 .into());
305 };
306 harn_vm::orchestration::register_vm_hook(
307 hook.event,
308 hook.pattern.clone(),
309 hook.handler.clone(),
310 closure.clone(),
311 );
312 }
313 Ok(())
314}
315
316pub async fn collect_manifest_triggers(
317 vm: &mut harn_vm::Vm,
318 extensions: &RuntimeExtensions,
319) -> Result<Vec<CollectedManifestTrigger>, PackageError> {
320 collect_manifest_triggers_with_mode(vm, extensions, false).await
321}
322
323async fn collect_manifest_triggers_with_mode(
324 vm: &mut harn_vm::Vm,
325 extensions: &RuntimeExtensions,
326 lazy_vm_callables: bool,
327) -> Result<Vec<CollectedManifestTrigger>, PackageError> {
328 let _provider_schema_guard = lock_manifest_provider_schemas().await;
329 let provider_schemas = build_manifest_provider_schemas(extensions).await?;
330 let provider_catalog = manifest_provider_catalog(provider_schemas.clone())?;
331 validate_orchestrator_budget(extensions.root_manifest.as_ref())?;
332 validate_static_trigger_configs(&extensions.triggers, &provider_catalog)?;
333 let mut loaded_exports: HashMap<ManifestModuleCacheKey, ManifestModuleExports> = HashMap::new();
334 let mut module_signatures: HashMap<PathBuf, Vec<CachedModuleCallableSignatures>> =
335 HashMap::new();
336 let mut validated = Vec::with_capacity(extensions.triggers.len());
337 for trigger in &extensions.triggers {
338 validated.push(validate_trigger_callable_declarations(
339 trigger,
340 &mut module_signatures,
341 )?);
342 }
343 let mut collected = Vec::new();
344
345 for (trigger, declarations) in extensions.triggers.iter().zip(validated) {
346 let mut effective_config = trigger.clone();
347 let collected_handler = match declarations.handler {
348 TriggerHandlerUri::Local(reference) => {
349 let module_path = declarations
350 .local_handler_path
351 .expect("validated local trigger handler has a source path");
352 let callable = collect_manifest_vm_callable(
353 vm,
354 &mut loaded_exports,
355 trigger,
356 &reference,
357 &module_path,
358 lazy_vm_callables,
359 "handler",
360 )
361 .await?;
362 CollectedTriggerHandler::Local {
363 reference,
364 callable,
365 }
366 }
367 TriggerHandlerUri::A2a {
368 target,
369 allow_cleartext,
370 } => CollectedTriggerHandler::A2a {
371 target,
372 allow_cleartext,
373 },
374 TriggerHandlerUri::Worker { queue } => CollectedTriggerHandler::Worker { queue },
375 TriggerHandlerUri::Persona { name } => {
376 let (binding, callable, autonomy_ceiling) =
377 persona_runtime_handler_for_trigger(extensions, trigger, &name)?;
378 effective_config.autonomy_tier =
379 effective_config.autonomy_tier.min(autonomy_ceiling);
380 CollectedTriggerHandler::Persona { binding, callable }
381 }
382 TriggerHandlerUri::EvalPack { target } => {
383 let manifest = eval_pack_manifest_for_handler(trigger, &target)?;
384 let ledger_options = eval_pack_ledger_options_for_handler(trigger)?;
385 CollectedTriggerHandler::EvalPack {
386 target,
387 manifest: Box::new(manifest),
388 ledger_options,
389 }
390 }
391 };
392
393 let collected_when = if let Some((reference, source_path)) = declarations.when {
394 let callable = collect_manifest_vm_callable(
395 vm,
396 &mut loaded_exports,
397 trigger,
398 &reference,
399 &source_path,
400 lazy_vm_callables,
401 "when predicate",
402 )
403 .await?;
404
405 Some(CollectedTriggerPredicate {
406 reference,
407 callable,
408 })
409 } else {
410 None
411 };
412
413 let flow_control = collect_trigger_flow_control(vm, trigger).await?;
414
415 collected.push(CollectedManifestTrigger {
416 config: effective_config,
417 handler: collected_handler,
418 when: collected_when,
419 flow_control,
420 });
421 }
422
423 register_manifest_provider_schemas(provider_schemas)?;
424 Ok(collected)
425}
426
427struct ValidatedTriggerCallableDeclarations {
428 handler: TriggerHandlerUri,
429 local_handler_path: Option<PathBuf>,
430 when: Option<(TriggerFunctionRef, PathBuf)>,
431}
432
433struct CachedModuleCallableSignatures {
434 execution_guard: Option<Arc<harn_modules::package_execution::PackageExecutionGuard>>,
435 signatures: BTreeMap<String, ModuleCallableSignature>,
436}
437
438fn validate_trigger_callable_declarations(
439 trigger: &ResolvedTriggerConfig,
440 module_signatures: &mut HashMap<PathBuf, Vec<CachedModuleCallableSignatures>>,
441) -> Result<ValidatedTriggerCallableDeclarations, PackageError> {
442 let handler = parse_trigger_handler_uri(trigger)?;
443 let local_handler_path = if let TriggerHandlerUri::Local(reference) = &handler {
444 let module_path = trigger_function_source_path(trigger, reference)?;
445 let signatures = cached_module_callable_signatures(
446 module_signatures,
447 &module_path,
448 trigger.execution_guard.as_ref(),
449 )
450 .map_err(|error| trigger_error(trigger, error))?;
451 if signatures
452 .get(&reference.function_name)
453 .is_none_or(|signature| !signature.is_pub)
454 {
455 return Err(trigger_error(
456 trigger,
457 format!(
458 "handler '{}' is not exported by the resolved module",
459 reference.raw
460 ),
461 ));
462 }
463 Some(module_path)
464 } else {
465 None
466 };
467 let when = if let Some(when_raw) = &trigger.when {
468 let reference = parse_local_trigger_ref(when_raw, "when", trigger)?;
469 let source_path = trigger_function_source_path(trigger, &reference)?;
470 let signatures = cached_module_callable_signatures(
471 module_signatures,
472 &source_path,
473 trigger.execution_guard.as_ref(),
474 )
475 .map_err(|error| trigger_error(trigger, error))?;
476 let Some(signature) = signatures.get(&reference.function_name) else {
477 return Err(trigger_error(
478 trigger,
479 format!(
480 "when predicate '{}' must resolve to a function declaration",
481 reference.raw
482 ),
483 ));
484 };
485 if !signature.is_pub {
486 return Err(trigger_error(
487 trigger,
488 format!(
489 "when predicate '{}' is not exported by the resolved module",
490 reference.raw
491 ),
492 ));
493 }
494 if signature.params.len() != 1
495 || signature.params[0]
496 .as_ref()
497 .is_none_or(|param| !is_trigger_event_type(param))
498 {
499 return Err(trigger_error(
500 trigger,
501 format!(
502 "when predicate '{}' must have signature fn(TriggerEvent) -> bool",
503 reference.raw
504 ),
505 ));
506 }
507 if signature
508 .return_type
509 .as_ref()
510 .is_none_or(|return_type| !is_predicate_return_type(return_type))
511 {
512 return Err(trigger_error(
513 trigger,
514 format!(
515 "when predicate '{}' must have signature fn(TriggerEvent) -> bool or Result<bool, _>",
516 reference.raw
517 ),
518 ));
519 }
520 Some((reference, source_path))
521 } else {
522 None
523 };
524 Ok(ValidatedTriggerCallableDeclarations {
525 handler,
526 local_handler_path,
527 when,
528 })
529}
530
531fn trigger_function_source_path(
532 trigger: &ResolvedTriggerConfig,
533 reference: &TriggerFunctionRef,
534) -> Result<PathBuf, PackageError> {
535 manifest_module_source_path(
536 &trigger.manifest_dir,
537 trigger.package_name.as_deref(),
538 &trigger.exports,
539 reference.module_name.as_deref(),
540 )
541 .map_err(|error| trigger_error(trigger, error))
542}
543
544async fn collect_manifest_vm_callable(
545 vm: &mut harn_vm::Vm,
546 loaded_exports: &mut HashMap<ManifestModuleCacheKey, ManifestModuleExports>,
547 trigger: &ResolvedTriggerConfig,
548 reference: &TriggerFunctionRef,
549 module_path: &Path,
550 lazy: bool,
551 role: &str,
552) -> Result<harn_vm::VmCallable, PackageError> {
553 let mut deferred =
554 harn_vm::LazyVmCallable::new(module_path.to_path_buf(), reference.function_name.clone());
555 if let Some(guard) = &trigger.execution_guard {
556 deferred = deferred.with_package_execution_guard(Arc::clone(guard));
557 }
558 if lazy {
559 return Ok(harn_vm::VmCallable::Lazy(deferred));
560 }
561 if trigger.execution_guard.is_some() {
562 let closure = vm
563 .resolve_callable(&harn_vm::VmCallable::Lazy(deferred))
564 .await
565 .map_err(|error| trigger_error(trigger, error.to_string()))?;
566 return Ok(harn_vm::VmCallable::Eager(closure));
567 }
568
569 let cache_key = (
570 trigger.manifest_dir.clone(),
571 trigger.package_name.clone(),
572 reference.module_name.clone(),
573 );
574 if !loaded_exports.contains_key(&cache_key) {
575 let exports = resolve_manifest_exports(
576 vm,
577 &trigger.manifest_dir,
578 trigger.package_name.as_deref(),
579 &trigger.exports,
580 reference.module_name.as_deref(),
581 )
582 .await
583 .map_err(|error| trigger_error(trigger, error))?;
584 loaded_exports.insert(cache_key.clone(), exports);
585 }
586 let exports = loaded_exports
587 .get(&cache_key)
588 .expect("manifest trigger exports cached");
589 let closure = exports.get(&reference.function_name).ok_or_else(|| {
590 trigger_error(
591 trigger,
592 format!(
593 "{role} '{}' is not exported by the resolved module",
594 reference.raw
595 ),
596 )
597 })?;
598 Ok(harn_vm::VmCallable::Eager(closure.clone()))
599}
600
601fn cached_module_callable_signatures<'a>(
602 cache: &'a mut HashMap<PathBuf, Vec<CachedModuleCallableSignatures>>,
603 source_path: &Path,
604 execution_guard: Option<&Arc<harn_modules::package_execution::PackageExecutionGuard>>,
605) -> Result<&'a BTreeMap<String, ModuleCallableSignature>, PackageError> {
606 let entries = cache.entry(source_path.to_path_buf()).or_default();
607 if let Some(index) = entries
608 .iter()
609 .position(|entry| entry.execution_guard.as_ref() == execution_guard)
610 {
611 return Ok(&entries[index].signatures);
612 }
613 let signatures = if let Some(guard) = execution_guard {
614 load_guarded_module_callable_signatures(source_path, guard)?
615 } else {
616 load_module_callable_signatures(source_path)?
617 };
618 entries.push(CachedModuleCallableSignatures {
619 execution_guard: execution_guard.cloned(),
620 signatures,
621 });
622 Ok(&entries
623 .last()
624 .expect("signature cache entry inserted")
625 .signatures)
626}
627
628pub(crate) async fn collect_trigger_flow_control(
629 vm: &mut harn_vm::Vm,
630 trigger: &ResolvedTriggerConfig,
631) -> Result<harn_vm::TriggerFlowControlConfig, PackageError> {
632 let mut flow = harn_vm::TriggerFlowControlConfig::default();
633
634 let concurrency = if let Some(spec) = &trigger.concurrency {
635 Some(spec.clone())
636 } else if let Some(max) = trigger.budget.max_concurrent {
637 eprintln!(
638 "warning: {} uses deprecated budget.max_concurrent; prefer concurrency = {{ max = {} }}",
639 manifest_trigger_location(trigger),
640 max
641 );
642 Some(TriggerConcurrencyManifestSpec { key: None, max })
643 } else {
644 None
645 };
646 if let Some(spec) = concurrency {
647 flow.concurrency = Some(harn_vm::TriggerConcurrencyConfig {
648 key: compile_optional_trigger_expression(
649 vm,
650 trigger,
651 "concurrency.key",
652 spec.key.as_deref(),
653 )
654 .await?,
655 max: spec.max,
656 });
657 }
658
659 if let Some(spec) = &trigger.throttle {
660 flow.throttle = Some(harn_vm::TriggerThrottleConfig {
661 key: compile_optional_trigger_expression(
662 vm,
663 trigger,
664 "throttle.key",
665 spec.key.as_deref(),
666 )
667 .await?,
668 period: harn_vm::parse_flow_control_duration(&spec.period)
669 .map_err(|error| trigger_error(trigger, format!("throttle.period {error}")))?,
670 max: spec.max,
671 });
672 }
673
674 if let Some(spec) = &trigger.rate_limit {
675 flow.rate_limit = Some(harn_vm::TriggerRateLimitConfig {
676 key: compile_optional_trigger_expression(
677 vm,
678 trigger,
679 "rate_limit.key",
680 spec.key.as_deref(),
681 )
682 .await?,
683 period: harn_vm::parse_flow_control_duration(&spec.period)
684 .map_err(|error| trigger_error(trigger, format!("rate_limit.period {error}")))?,
685 max: spec.max,
686 });
687 }
688
689 if let Some(spec) = &trigger.debounce {
690 flow.debounce = Some(harn_vm::TriggerDebounceConfig {
691 key: compile_trigger_expression(vm, trigger, "debounce.key", &spec.key).await?,
692 period: harn_vm::parse_flow_control_duration(&spec.period)
693 .map_err(|error| trigger_error(trigger, format!("debounce.period {error}")))?,
694 });
695 }
696
697 if let Some(spec) = &trigger.singleton {
698 flow.singleton = Some(harn_vm::TriggerSingletonConfig {
699 key: compile_optional_trigger_expression(
700 vm,
701 trigger,
702 "singleton.key",
703 spec.key.as_deref(),
704 )
705 .await?,
706 });
707 }
708
709 if let Some(spec) = &trigger.batch {
710 flow.batch = Some(harn_vm::TriggerBatchConfig {
711 key: compile_optional_trigger_expression(vm, trigger, "batch.key", spec.key.as_deref())
712 .await?,
713 size: spec.size,
714 timeout: harn_vm::parse_flow_control_duration(&spec.timeout)
715 .map_err(|error| trigger_error(trigger, format!("batch.timeout {error}")))?,
716 });
717 }
718
719 if let Some(spec) = &trigger.priority_flow {
720 flow.priority = Some(harn_vm::TriggerPriorityOrderConfig {
721 key: compile_trigger_expression(vm, trigger, "priority.key", &spec.key).await?,
722 order: spec.order.clone(),
723 });
724 }
725
726 Ok(flow)
727}
728
729fn eval_pack_manifest_for_handler(
730 trigger: &ResolvedTriggerConfig,
731 target: &str,
732) -> Result<harn_vm::orchestration::EvalPackManifest, PackageError> {
733 if eval_pack_target_is_path(target) {
734 let path = resolve_eval_pack_target_path(&trigger.manifest_dir, target);
735 return harn_vm::orchestration::load_eval_pack_manifest(&path).map_err(|error| {
736 trigger_error(
737 trigger,
738 format!(
739 "handler eval_pack://{target} failed to load eval pack {}: {error}",
740 path.display()
741 ),
742 )
743 });
744 }
745
746 let paths = load_package_eval_pack_paths(Some(&trigger.manifest_path))
747 .map_err(|error| trigger_error(trigger, error))?;
748 let mut matches = Vec::new();
749 for path in paths {
750 let manifest = harn_vm::orchestration::load_eval_pack_manifest(&path).map_err(|error| {
751 trigger_error(
752 trigger,
753 format!(
754 "failed to load package eval pack {}: {error}",
755 path.display()
756 ),
757 )
758 })?;
759 let file_stem = path.file_stem().and_then(|stem| stem.to_str());
760 if manifest.id == target
761 || manifest.name.as_deref() == Some(target)
762 || file_stem == Some(target)
763 {
764 matches.push((path, manifest));
765 }
766 }
767
768 match matches.len() {
769 0 => Err(trigger_error(
770 trigger,
771 format!(
772 "handler eval_pack://{target} did not match any package eval pack by id, name, or file stem",
773 ),
774 )),
775 1 => Ok(matches.remove(0).1),
776 _ => Err(trigger_error(
777 trigger,
778 format!("handler eval_pack://{target} matched multiple package eval packs"),
779 )),
780 }
781}
782
783fn eval_pack_target_is_path(target: &str) -> bool {
784 target.contains('/')
785 || target.contains('\\')
786 || target.ends_with(".toml")
787 || target.ends_with(".json")
788}
789
790fn resolve_eval_pack_target_path(manifest_dir: &Path, target: &str) -> PathBuf {
791 let path = PathBuf::from(target);
792 if path.is_absolute() {
793 path
794 } else {
795 manifest_dir.join(path)
796 }
797}
798
799fn eval_pack_ledger_options_for_handler(
800 trigger: &ResolvedTriggerConfig,
801) -> Result<Option<serde_json::Value>, PackageError> {
802 let value = trigger
803 .kind_specific
804 .get("eval_options")
805 .or_else(|| trigger.kind_specific.get("ledger"));
806 value
807 .map(|value| {
808 serde_json::to_value(value).map_err(|error| {
809 trigger_error(trigger, format!("invalid eval ledger options: {error}"))
810 })
811 })
812 .transpose()
813}
814
815pub(crate) async fn compile_optional_trigger_expression(
816 vm: &mut harn_vm::Vm,
817 trigger: &ResolvedTriggerConfig,
818 field_name: &str,
819 expr: Option<&str>,
820) -> Result<Option<harn_vm::TriggerExpressionSpec>, PackageError> {
821 match expr {
822 Some(expr) => compile_trigger_expression(vm, trigger, field_name, expr)
823 .await
824 .map(Some),
825 None => Ok(None),
826 }
827}
828
829pub(crate) async fn compile_trigger_expression(
830 vm: &mut harn_vm::Vm,
831 trigger: &ResolvedTriggerConfig,
832 field_name: &str,
833 expr: &str,
834) -> Result<harn_vm::TriggerExpressionSpec, PackageError> {
835 let synthetic = PathBuf::from(format!(
836 "<trigger-expr>/{}/{:04}-{}.harn",
837 harn_vm::event_log::sanitize_topic_component(&trigger.id),
838 trigger.table_index,
839 harn_vm::event_log::sanitize_topic_component(field_name),
840 ));
841 let source = format!(
842 "import \"std/triggers\"\n\npub fn __trigger_expr(event: TriggerEvent) -> any {{\n return {expr}\n}}\n"
843 );
844 let exports = vm
845 .load_module_exports_from_source(synthetic, &source)
846 .await
847 .map_err(|error| {
848 trigger_error(
849 trigger,
850 format!("{field_name} '{expr}' is invalid Harn expression: {error}"),
851 )
852 })?;
853 let closure = exports.get("__trigger_expr").ok_or_else(|| {
854 trigger_error(
855 trigger,
856 format!("{field_name} '{expr}' did not compile into an exported closure"),
857 )
858 })?;
859 Ok(harn_vm::TriggerExpressionSpec {
860 raw: expr.to_string(),
861 callable: harn_vm::VmCallable::Eager(closure.clone()),
862 })
863}
864
865pub(crate) fn trigger_kind_label(kind: TriggerKind) -> &'static str {
866 match kind {
867 TriggerKind::Webhook => "webhook",
868 TriggerKind::Cron => "cron",
869 TriggerKind::Poll => "poll",
870 TriggerKind::Stream => "stream",
871 TriggerKind::Predicate => "predicate",
872 TriggerKind::A2aPush => "a2a-push",
873 }
874}
875
876pub(crate) fn worker_queue_priority(
877 priority: TriggerDispatchPriority,
878) -> harn_vm::WorkerQueuePriority {
879 match priority {
880 TriggerDispatchPriority::High => harn_vm::WorkerQueuePriority::High,
881 TriggerDispatchPriority::Normal => harn_vm::WorkerQueuePriority::Normal,
882 TriggerDispatchPriority::Low => harn_vm::WorkerQueuePriority::Low,
883 }
884}
885
886pub fn manifest_trigger_binding_spec(
887 trigger: CollectedManifestTrigger,
888) -> harn_vm::TriggerBindingSpec {
889 let flow_control = trigger.flow_control.clone();
890 let config = trigger.config;
891 let (handler, handler_descriptor) = match trigger.handler {
892 CollectedTriggerHandler::Local {
893 reference,
894 callable,
895 } => (
896 harn_vm::TriggerHandlerSpec::Local {
897 raw: reference.raw.clone(),
898 callable,
899 },
900 serde_json::json!({
901 "kind": "local",
902 "raw": reference.raw,
903 }),
904 ),
905 CollectedTriggerHandler::A2a {
906 target,
907 allow_cleartext,
908 } => (
909 harn_vm::TriggerHandlerSpec::A2a {
910 target: target.clone(),
911 allow_cleartext,
912 },
913 serde_json::json!({
914 "kind": "a2a",
915 "target": target,
916 "allow_cleartext": allow_cleartext,
917 }),
918 ),
919 CollectedTriggerHandler::Worker { queue } => (
920 harn_vm::TriggerHandlerSpec::Worker {
921 queue: queue.clone(),
922 },
923 serde_json::json!({
924 "kind": "worker",
925 "queue": queue,
926 }),
927 ),
928 CollectedTriggerHandler::Persona { binding, callable } => (
929 harn_vm::TriggerHandlerSpec::Persona {
930 binding: binding.clone(),
931 callable,
932 },
933 serde_json::json!({
934 "kind": "persona",
935 "name": binding.name,
936 "entry_workflow": binding.entry_workflow,
937 }),
938 ),
939 CollectedTriggerHandler::EvalPack {
940 target,
941 manifest,
942 ledger_options,
943 } => {
944 let pack_id = manifest.id.clone();
945 let harness_config_fingerprint =
946 harn_vm::orchestration::eval_pack_harness_config_fingerprint(manifest.as_ref())
947 .ok();
948 (
949 harn_vm::TriggerHandlerSpec::EvalPack {
950 target: target.clone(),
951 manifest,
952 ledger_options: ledger_options.clone(),
953 },
954 serde_json::json!({
955 "kind": "eval_pack",
956 "target": target,
957 "pack_id": pack_id,
958 "harness_config_fingerprint": harness_config_fingerprint,
959 "ledger_options": ledger_options,
960 }),
961 )
962 }
963 };
964
965 let when_raw = trigger
966 .when
967 .as_ref()
968 .map(|predicate| predicate.reference.raw.clone());
969 let when = trigger.when.map(|predicate| harn_vm::TriggerPredicateSpec {
970 raw: predicate.reference.raw,
971 callable: predicate.callable,
972 });
973 let mut when_budget = config
974 .when_budget
975 .as_ref()
976 .map(|budget| {
977 Ok::<harn_vm::TriggerPredicateBudget, String>(harn_vm::TriggerPredicateBudget {
978 max_cost_usd: budget.max_cost_usd,
979 tokens_max: budget.tokens_max,
980 timeout_ms: budget
981 .timeout
982 .as_deref()
983 .map(parse_duration_millis)
984 .transpose()?,
985 })
986 })
987 .transpose()
988 .unwrap_or_default();
989 if config.budget.max_cost_usd.is_some() || config.budget.max_tokens.is_some() {
990 let budget = when_budget.get_or_insert_with(harn_vm::TriggerPredicateBudget::default);
991 if budget.max_cost_usd.is_none() {
992 budget.max_cost_usd = config.budget.max_cost_usd;
993 }
994 if budget.tokens_max.is_none() {
995 budget.tokens_max = config.budget.max_tokens;
996 }
997 }
998 let id = config.id.clone();
999 let kind = trigger_kind_label(config.kind).to_string();
1000 let provider = config.provider.clone();
1001 let autonomy_tier = config.autonomy_tier;
1002 let match_events = config.match_.events.clone();
1003 let dedupe_key = config.dedupe_key.clone();
1004 let retry = harn_vm::TriggerRetryConfig::new(
1005 config.retry.max,
1006 match config.retry.backoff {
1007 TriggerRetryBackoff::Immediate => harn_vm::RetryPolicy::Linear { delay_ms: 0 },
1008 TriggerRetryBackoff::Svix => harn_vm::RetryPolicy::Svix,
1009 },
1010 );
1011 let filter = config.filter.clone();
1012 let dedupe_retention_days = config.retry.retention_days;
1013 let daily_cost_usd = config.budget.daily_cost_usd;
1014 let hourly_cost_usd = config.budget.hourly_cost_usd;
1015 let max_autonomous_decisions_per_hour = config.budget.max_autonomous_decisions_per_hour;
1016 let max_autonomous_decisions_per_day = config.budget.max_autonomous_decisions_per_day;
1017 let on_budget_exhausted = config.budget.on_budget_exhausted;
1018 let max_concurrent = flow_control.concurrency.as_ref().map(|config| config.max);
1019 let manifest_path = Some(config.manifest_path.clone());
1020 let package_name = config.package_name.clone();
1021
1022 let fingerprint = serde_json::to_string(&serde_json::json!({
1023 "id": &id,
1024 "kind": &kind,
1025 "provider": provider.as_str(),
1026 "autonomy_tier": autonomy_tier,
1027 "match": config.match_,
1028 "when": when_raw,
1029 "when_budget": config.when_budget,
1030 "handler": handler_descriptor,
1031 "dedupe_key": &dedupe_key,
1032 "retry": config.retry,
1033 "dispatch_priority": config.dispatch_priority,
1034 "budget": config.budget,
1035 "flow_control": {
1036 "concurrency": config.concurrency,
1037 "throttle": config.throttle,
1038 "rate_limit": config.rate_limit,
1039 "debounce": config.debounce,
1040 "singleton": config.singleton,
1041 "batch": config.batch,
1042 "priority": config.priority_flow,
1043 },
1044 "window": config.window,
1045 "secrets": config.secrets,
1046 "filter": &filter,
1047 "kind_specific": config.kind_specific,
1048 "manifest_path": &manifest_path,
1049 "package_name": &package_name,
1050 }))
1051 .unwrap_or_else(|_| format!("{}:{}:{}", id, kind, provider.as_str()));
1052
1053 harn_vm::TriggerBindingSpec {
1054 id,
1055 source: harn_vm::TriggerBindingSource::Manifest,
1056 kind,
1057 provider,
1058 autonomy_tier,
1059 handler,
1060 dispatch_priority: worker_queue_priority(config.dispatch_priority),
1061 when,
1062 when_budget,
1063 retry,
1064 match_events,
1065 dedupe_key,
1066 filter,
1067 dedupe_retention_days,
1068 daily_cost_usd,
1069 hourly_cost_usd,
1070 max_autonomous_decisions_per_hour,
1071 max_autonomous_decisions_per_day,
1072 on_budget_exhausted,
1073 max_concurrent,
1074 flow_control,
1075 aggregation: None,
1076 manifest_path,
1077 package_name,
1078 definition_fingerprint: fingerprint,
1079 }
1080}
1081
1082pub async fn install_manifest_triggers(
1083 vm: &mut harn_vm::Vm,
1084 extensions: &RuntimeExtensions,
1085) -> Result<(), PackageError> {
1086 install_manifest_triggers_with_mode(vm, extensions, false).await
1087}
1088
1089pub async fn install_manifest_triggers_with_mode(
1094 vm: &mut harn_vm::Vm,
1095 extensions: &RuntimeExtensions,
1096 lazy_vm_callables: bool,
1097) -> Result<(), PackageError> {
1098 install_orchestrator_budget(extensions);
1099 let collected = collect_manifest_triggers_with_mode(vm, extensions, lazy_vm_callables).await?;
1100 let mut bindings: Vec<_> = collected
1101 .iter()
1102 .cloned()
1103 .map(manifest_trigger_binding_spec)
1104 .collect();
1105 bindings.extend(collect_persona_trigger_binding_specs(extensions)?);
1106 harn_vm::install_manifest_triggers(bindings)
1107 .await
1108 .map_err(|error| PackageError::Extensions(error.to_string()))
1109}
1110
1111pub async fn install_collected_manifest_triggers(
1112 collected: &[CollectedManifestTrigger],
1113) -> Result<(), PackageError> {
1114 let bindings = collected
1115 .iter()
1116 .cloned()
1117 .map(manifest_trigger_binding_spec)
1118 .collect();
1119 harn_vm::install_manifest_triggers(bindings)
1120 .await
1121 .map_err(|error| PackageError::Extensions(error.to_string()))
1122}
1123
1124pub fn load_personas_from_manifest_path(
1125 manifest_path: &Path,
1126) -> Result<ResolvedPersonaManifest, Vec<PersonaValidationError>> {
1127 let manifest_path = if manifest_path.is_dir() {
1128 manifest_path.join(MANIFEST)
1129 } else {
1130 manifest_path.to_path_buf()
1131 };
1132 let manifest_dir = manifest_path
1133 .parent()
1134 .map(Path::to_path_buf)
1135 .unwrap_or_else(|| PathBuf::from("."));
1136 if manifest_path.extension().and_then(|ext| ext.to_str()) == Some("harn") {
1137 return match harn_modules::personas::parse_persona_source_file(&manifest_path) {
1138 Ok(document) if !document.personas.is_empty() => {
1139 validate_and_resolve_standalone_personas(
1140 document.personas,
1141 manifest_path,
1142 manifest_dir,
1143 )
1144 }
1145 Ok(_) => Err(vec![PersonaValidationError {
1146 manifest_path: manifest_path.clone(),
1147 field_path: "persona".to_string(),
1148 message: "no @persona declarations found".to_string(),
1149 }]),
1150 Err(message) => Err(vec![PersonaValidationError {
1151 manifest_path: manifest_path.clone(),
1152 field_path: "persona".to_string(),
1153 message,
1154 }]),
1155 };
1156 }
1157 let manifest = match read_manifest_from_path(&manifest_path) {
1158 Ok(manifest) => manifest,
1159 Err(message) => {
1160 if let Ok(document) =
1161 harn_modules::personas::parse_persona_manifest_file(&manifest_path)
1162 {
1163 if !document.personas.is_empty() {
1164 return validate_and_resolve_standalone_personas(
1165 document.personas,
1166 manifest_path,
1167 manifest_dir,
1168 );
1169 }
1170 }
1171 return Err(vec![PersonaValidationError {
1172 manifest_path: manifest_path.clone(),
1173 field_path: "harn.toml".to_string(),
1174 message: message.to_string(),
1175 }]);
1176 }
1177 };
1178 if manifest.personas.is_empty() {
1179 if let Ok(document) = harn_modules::personas::parse_persona_manifest_file(&manifest_path) {
1180 if !document.personas.is_empty() {
1181 return validate_and_resolve_standalone_personas(
1182 document.personas,
1183 manifest_path,
1184 manifest_dir,
1185 );
1186 }
1187 }
1188 }
1189 validate_and_resolve_personas(manifest, manifest_path, manifest_dir)
1190}
1191
1192pub(crate) fn load_personas_from_verified_package_manifest(
1193 manifest_path: &Path,
1194 source: &str,
1195) -> Result<ResolvedPersonaManifest, Vec<PersonaValidationError>> {
1196 let manifest_path = manifest_path.to_path_buf();
1197 let manifest_dir = manifest_path
1198 .parent()
1199 .map(Path::to_path_buf)
1200 .unwrap_or_else(|| PathBuf::from("."));
1201 let manifest = toml::from_str::<Manifest>(source).map_err(|error| {
1202 vec![PersonaValidationError {
1203 manifest_path: manifest_path.clone(),
1204 field_path: "harn.toml".to_string(),
1205 message: format!("failed to parse {}: {error}", manifest_path.display()),
1206 }]
1207 })?;
1208 validate_and_resolve_personas(manifest, manifest_path, manifest_dir)
1209}
1210
1211fn validate_and_resolve_standalone_personas(
1212 personas: Vec<PersonaManifestEntry>,
1213 manifest_path: PathBuf,
1214 manifest_dir: PathBuf,
1215) -> Result<ResolvedPersonaManifest, Vec<PersonaValidationError>> {
1216 let known_names = personas
1217 .iter()
1218 .filter_map(|persona| persona.name.as_ref())
1219 .filter(|name| !name.trim().is_empty())
1220 .cloned()
1221 .collect();
1222 let context = harn_modules::personas::PersonaValidationContext {
1223 known_capabilities: harn_modules::personas::default_persona_capabilities(),
1224 known_tools: BTreeSet::new(),
1225 known_names,
1226 };
1227 harn_modules::personas::validate_persona_manifests(&manifest_path, &personas, &context)?;
1228 Ok(ResolvedPersonaManifest {
1229 manifest_path,
1230 manifest_dir,
1231 personas,
1232 })
1233}
1234
1235pub fn load_personas_config(
1236 anchor: Option<&Path>,
1237) -> Result<Option<ResolvedPersonaManifest>, Vec<PersonaValidationError>> {
1238 let anchor = anchor
1239 .map(Path::to_path_buf)
1240 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1241 let Some((manifest, dir)) = nearest_manifest_or_warn(&anchor) else {
1242 return Ok(None);
1243 };
1244 let manifest_path = dir.join(MANIFEST);
1245 validate_and_resolve_personas(manifest, manifest_path, dir).map(Some)
1246}
1247
1248pub(crate) fn validate_and_resolve_personas(
1249 manifest: Manifest,
1250 manifest_path: PathBuf,
1251 manifest_dir: PathBuf,
1252) -> Result<ResolvedPersonaManifest, Vec<PersonaValidationError>> {
1253 let known_capabilities = known_persona_capabilities(&manifest, &manifest_dir);
1254 let known_tools = known_persona_tools(&manifest);
1255 let known_names: BTreeSet<String> = manifest
1256 .personas
1257 .iter()
1258 .filter_map(|persona| persona.name.as_ref())
1259 .filter(|name| !name.trim().is_empty())
1260 .cloned()
1261 .collect();
1262 let context = harn_modules::personas::PersonaValidationContext {
1263 known_capabilities,
1264 known_tools,
1265 known_names,
1266 };
1267 if let Err(errors) = harn_modules::personas::validate_persona_manifests(
1268 &manifest_path,
1269 &manifest.personas,
1270 &context,
1271 ) {
1272 Err(errors)
1273 } else {
1274 let mut personas = manifest.personas;
1275 attach_entry_workflow_steps(&mut personas, &manifest_dir);
1276 Ok(ResolvedPersonaManifest {
1277 manifest_path,
1278 manifest_dir,
1279 personas,
1280 })
1281 }
1282}
1283
1284fn attach_entry_workflow_steps(personas: &mut [PersonaManifestEntry], manifest_dir: &Path) {
1285 for persona in personas {
1286 if !persona.steps.is_empty() {
1287 continue;
1288 }
1289 let Some(entry_workflow) = persona.entry_workflow.as_deref() else {
1290 continue;
1291 };
1292 let Some((path, entry_name)) = entry_workflow.split_once('#') else {
1293 continue;
1294 };
1295 if !path.ends_with(".harn") {
1296 continue;
1297 }
1298 let source_path = manifest_dir.join(path);
1299 let Ok(document) = harn_modules::personas::parse_persona_source_file(&source_path) else {
1300 continue;
1301 };
1302 let entry_name = entry_name.trim();
1303 if let Some(source_persona) = document.personas.iter().find(|candidate| {
1304 candidate.entry_workflow.as_deref() == Some(entry_name)
1305 || candidate.name.as_deref() == persona.name.as_deref()
1306 }) {
1307 persona.steps.clone_from(&source_persona.steps);
1308 }
1309 }
1310}
1311
1312pub(crate) fn known_persona_capabilities(
1313 manifest: &Manifest,
1314 manifest_dir: &Path,
1315) -> BTreeSet<String> {
1316 let mut capabilities = BTreeSet::new();
1317 for (capability, operations) in default_persona_capability_map() {
1318 for operation in operations {
1319 capabilities.insert(format!("{capability}.{operation}"));
1320 }
1321 }
1322 for (capability, operations) in &manifest.check.host_capabilities {
1323 for operation in operations {
1324 capabilities.insert(format!("{capability}.{operation}"));
1325 }
1326 }
1327 if let Some(path) = manifest.check.host_capabilities_path.as_deref() {
1328 let path = PathBuf::from(path);
1329 let path = if path.is_absolute() {
1330 path
1331 } else {
1332 manifest_dir.join(path)
1333 };
1334 if let Ok(content) = fs::read_to_string(path) {
1335 let parsed_json = serde_json::from_str::<serde_json::Value>(&content).ok();
1336 let parsed_toml = toml::from_str::<toml::Value>(&content)
1337 .ok()
1338 .and_then(|value| serde_json::to_value(value).ok());
1339 if let Some(value) = parsed_json.or(parsed_toml) {
1340 collect_persona_capabilities_from_json(&value, &mut capabilities);
1341 }
1342 }
1343 }
1344 capabilities
1345}
1346
1347pub(crate) fn collect_persona_capabilities_from_json(
1348 value: &serde_json::Value,
1349 out: &mut BTreeSet<String>,
1350) {
1351 let root = value.get("capabilities").unwrap_or(value);
1352 let Some(capabilities) = root.as_object() else {
1353 return;
1354 };
1355 for (capability, entry) in capabilities {
1356 if let Some(list) = entry.as_array() {
1357 for item in list {
1358 if let Some(operation) = item.as_str() {
1359 out.insert(format!("{capability}.{operation}"));
1360 }
1361 }
1362 } else if let Some(obj) = entry.as_object() {
1363 if let Some(list) = obj
1364 .get("operations")
1365 .or_else(|| obj.get("ops"))
1366 .and_then(|v| v.as_array())
1367 {
1368 for item in list {
1369 if let Some(operation) = item.as_str() {
1370 out.insert(format!("{capability}.{operation}"));
1371 }
1372 }
1373 } else {
1374 for (operation, enabled) in obj {
1375 if enabled.as_bool().unwrap_or(true) {
1376 out.insert(format!("{capability}.{operation}"));
1377 }
1378 }
1379 }
1380 }
1381 }
1382}
1383
1384pub(crate) fn default_persona_capability_map() -> BTreeMap<&'static str, Vec<&'static str>> {
1385 harn_modules::personas::default_persona_capability_map()
1386}
1387
1388pub(crate) fn known_persona_tools(manifest: &Manifest) -> BTreeSet<String> {
1389 let mut tools = BTreeSet::from([
1390 "a2a".to_string(),
1391 "acp".to_string(),
1392 "ci".to_string(),
1393 "filesystem".to_string(),
1394 "github".to_string(),
1395 "linear".to_string(),
1396 "mcp".to_string(),
1397 "notion".to_string(),
1398 "pagerduty".to_string(),
1399 "shell".to_string(),
1400 "slack".to_string(),
1401 ]);
1402 for server in &manifest.mcp {
1403 tools.insert(server.name.clone());
1404 }
1405 for provider in &manifest.providers {
1406 tools.insert(provider.id.as_str().to_string());
1407 }
1408 for trigger in &manifest.triggers {
1409 if let Some(provider) = trigger.provider.as_ref() {
1410 tools.insert(provider.as_str().to_string());
1411 }
1412 for source in &trigger.sources {
1413 tools.insert(source.provider.as_str().to_string());
1414 }
1415 }
1416 tools
1417}
1418
1419#[cfg(test)]
1420#[path = "extensions_tests.rs"]
1421mod tests;
1422
1423#[cfg(test)]
1424#[path = "extensions_lazy_tests.rs"]
1425mod lazy_tests;
1426
1427#[cfg(test)]
1428#[path = "extensions_provider_tests.rs"]
1429mod provider_tests;
1430
1431#[cfg(test)]
1432#[path = "persona_runtime_tests.rs"]
1433mod persona_tests;