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