Skip to main content

harn_cli/package/
extensions.rs

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