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