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