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
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
79pub 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
164pub 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
195pub 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
1015pub 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;