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