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