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