Skip to main content

harn_cli/package/
extensions.rs

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