1use super::errors::PackageError;
2use super::*;
3mod check_config;
4mod connector_module;
5mod provider_setup;
6pub(crate) use check_config::absolutize_check_config_paths;
7pub use check_config::{load_check_config, CheckConfig, PreflightSeverity};
8pub use connector_module::is_declared_connector_module;
9pub use harn_modules::personas::{
10 PersonaAutonomyTier, PersonaManifestEntry, PersonaStageDecl, PersonaStageExit,
11 PersonaValidationError, ResolvedPersonaManifest,
12};
13pub use provider_setup::{
14 connector_service_issues, ConnectorConditionalProfileRequirement,
15 ConnectorConfigurationEnvironmentManifest, ConnectorCredentialEnvironmentManifest,
16 ConnectorEnvironment, ConnectorEvidenceRequirement, ConnectorExternalSpend,
17 ConnectorHealthCheckManifest, ConnectorOperationEffect, ConnectorOperationManifest,
18 ConnectorParameterManifest, ConnectorParameterType, ConnectorProtectedProfileManifest,
19 ConnectorReconciliation, ConnectorRecoveryCopy, ConnectorRedactionTarget,
20 ConnectorServiceManifest, ConnectorSetupConfigurationField, ConnectorTestProfile,
21 ProtectedProfileFieldClass, ProviderManifestEntry, ProviderSetupManifest,
22 ResolvedProviderConnectorConfig,
23};
24
25#[derive(Debug, Clone, Deserialize)]
26pub struct Manifest {
27 pub package: Option<PackageInfo>,
28 #[serde(default)]
29 pub dependencies: HashMap<String, Dependency>,
30 #[serde(default)]
31 pub mcp: Vec<McpServerConfig>,
32 #[serde(default)]
33 pub check: CheckConfig,
34 #[serde(default)]
35 pub workspace: WorkspaceConfig,
36 #[serde(default)]
39 pub registry: PackageRegistryConfig,
40 #[serde(default)]
43 pub skills: SkillsConfig,
44 #[serde(default)]
47 pub skill: SkillTables,
48 #[serde(default)]
56 pub capabilities: Option<harn_vm::llm::capabilities::CapabilitiesFile>,
57 #[serde(default)]
61 pub exports: HashMap<String, String>,
62 #[serde(default)]
67 pub llm: harn_vm::llm_config::ProvidersConfig,
68 #[serde(default)]
73 pub hooks: Vec<HookConfig>,
74 #[serde(default)]
79 pub triggers: Vec<TriggerManifestEntry>,
80 #[serde(default)]
84 pub handoff_routes: Vec<harn_vm::HandoffRouteConfig>,
85 #[serde(default)]
89 pub providers: Vec<ProviderManifestEntry>,
90 #[serde(default)]
94 pub personas: Vec<PersonaManifestEntry>,
95 #[serde(default, alias = "connector-contract")]
98 pub connector_contract: ConnectorContractConfig,
99 #[serde(default)]
102 pub orchestrator: OrchestratorConfig,
103 #[serde(default)]
107 pub rules: RulesConfig,
108 #[serde(default)]
118 pub contributes: Vec<ContributionEntry>,
119}
120
121#[derive(Debug, Clone, Deserialize, Serialize)]
136pub struct ContributionEntry {
137 pub kind: String,
142 pub id: String,
144 #[serde(default)]
145 pub title: Option<String>,
146 #[serde(default)]
149 pub when: Option<String>,
150 #[serde(default)]
153 pub scopes: Vec<String>,
154 #[serde(default)]
158 pub platforms: Vec<String>,
159 #[serde(flatten)]
161 pub config: BTreeMap<String, toml::Value>,
162}
163
164impl ContributionEntry {
165 pub fn has_namespaced_kind(&self) -> bool {
169 let mut segments = 0usize;
170 for segment in self.kind.split('.') {
171 if segment.is_empty()
172 || !segment
173 .chars()
174 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
175 || !segment.starts_with(|c: char| c.is_ascii_lowercase())
176 {
177 return false;
178 }
179 segments += 1;
180 }
181 segments >= 2
182 }
183}
184
185#[derive(Debug, Clone, Default, Deserialize)]
197pub struct RulesConfig {
198 #[serde(default, alias = "rule-dirs", alias = "ruleDirs")]
200 pub rule_dirs: Vec<String>,
201 #[serde(default, alias = "util-dirs", alias = "utilDirs")]
203 pub util_dirs: Vec<String>,
204 #[serde(default, alias = "test-configs", alias = "testConfigs")]
206 pub test_configs: Vec<String>,
207 #[serde(
209 default,
210 alias = "native-rule-dirs",
211 alias = "nativeRuleDirs",
212 alias = "native_rule_dirs"
213 )]
214 pub native_rule_dirs: Vec<String>,
215}
216
217#[derive(Debug, Clone, Default, Deserialize)]
218pub struct OrchestratorConfig {
219 #[serde(default, alias = "allowed-origins")]
220 pub allowed_origins: Vec<String>,
221 #[serde(default, alias = "max-body-bytes")]
222 pub max_body_bytes: Option<usize>,
223 #[serde(default)]
224 pub budget: OrchestratorBudgetSpec,
225 #[serde(default)]
226 pub drain: OrchestratorDrainConfig,
227 #[serde(default)]
228 pub pumps: OrchestratorPumpConfig,
229}
230
231#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
232pub struct OrchestratorBudgetSpec {
233 #[serde(default)]
234 pub daily_cost_usd: Option<f64>,
235 #[serde(default)]
236 pub hourly_cost_usd: Option<f64>,
237}
238
239#[derive(Debug, Clone, Deserialize)]
240pub struct OrchestratorDrainConfig {
241 #[serde(default = "default_orchestrator_drain_max_items", alias = "max-items")]
242 pub max_items: usize,
243 #[serde(
244 default = "default_orchestrator_drain_deadline_seconds",
245 alias = "deadline-seconds"
246 )]
247 pub deadline_seconds: u64,
248}
249
250impl Default for OrchestratorDrainConfig {
251 fn default() -> Self {
252 Self {
253 max_items: default_orchestrator_drain_max_items(),
254 deadline_seconds: default_orchestrator_drain_deadline_seconds(),
255 }
256 }
257}
258
259pub(crate) fn default_orchestrator_drain_max_items() -> usize {
260 1024
261}
262
263pub(crate) fn default_orchestrator_drain_deadline_seconds() -> u64 {
264 30
265}
266
267#[derive(Debug, Clone, Deserialize)]
268pub struct OrchestratorPumpConfig {
269 #[serde(
270 default = "default_orchestrator_pump_max_outstanding",
271 alias = "max-outstanding"
272 )]
273 pub max_outstanding: usize,
274}
275
276impl Default for OrchestratorPumpConfig {
277 fn default() -> Self {
278 Self {
279 max_outstanding: default_orchestrator_pump_max_outstanding(),
280 }
281 }
282}
283
284pub(crate) fn default_orchestrator_pump_max_outstanding() -> usize {
285 64
286}
287
288#[derive(Debug, Clone, Deserialize)]
289pub struct HookConfig {
290 pub event: harn_vm::orchestration::HookEvent,
291 #[serde(default = "default_hook_pattern")]
292 pub pattern: String,
293 pub handler: String,
294}
295
296pub(crate) fn default_hook_pattern() -> String {
297 "*".to_string()
298}
299
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
301pub struct TriggerManifestEntry {
302 pub id: String,
303 #[serde(default)]
304 pub kind: Option<TriggerKind>,
305 #[serde(default)]
306 pub provider: Option<harn_vm::ProviderId>,
307 #[serde(default, alias = "tier")]
308 pub autonomy_tier: harn_vm::AutonomyTier,
309 #[serde(default, rename = "match")]
310 pub match_: Option<TriggerMatchExpr>,
311 #[serde(default)]
312 pub sources: Vec<TriggerSourceManifestEntry>,
313 #[serde(default)]
314 pub when: Option<String>,
315 #[serde(default)]
316 pub when_budget: Option<TriggerWhenBudgetSpec>,
317 pub handler: String,
318 #[serde(default)]
319 pub dedupe_key: Option<String>,
320 #[serde(default)]
321 pub retry: TriggerRetrySpec,
322 #[serde(default)]
323 pub priority: Option<TriggerPriorityField>,
324 #[serde(default)]
325 pub budget: TriggerBudgetSpec,
326 #[serde(default)]
327 pub concurrency: Option<TriggerConcurrencyManifestSpec>,
328 #[serde(default)]
329 pub throttle: Option<TriggerThrottleManifestSpec>,
330 #[serde(default)]
331 pub rate_limit: Option<TriggerRateLimitManifestSpec>,
332 #[serde(default)]
333 pub debounce: Option<TriggerDebounceManifestSpec>,
334 #[serde(default)]
335 pub singleton: Option<TriggerSingletonManifestSpec>,
336 #[serde(default)]
337 pub batch: Option<TriggerBatchManifestSpec>,
338 #[serde(default)]
339 pub window: Option<TriggerStreamWindowManifestSpec>,
340 #[serde(default, alias = "dlq-alerts")]
341 pub dlq_alerts: Vec<TriggerDlqAlertManifestSpec>,
342 #[serde(default)]
343 pub secrets: BTreeMap<String, String>,
344 #[serde(default)]
345 pub filter: Option<String>,
346 #[serde(flatten, default)]
347 pub kind_specific: BTreeMap<String, toml::Value>,
348}
349
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
351pub struct TriggerSourceManifestEntry {
352 #[serde(default)]
353 pub id: Option<String>,
354 pub kind: TriggerKind,
355 pub provider: harn_vm::ProviderId,
356 #[serde(default, rename = "match")]
357 pub match_: Option<TriggerMatchExpr>,
358 #[serde(default)]
359 pub dedupe_key: Option<String>,
360 #[serde(default)]
361 pub retry: Option<TriggerRetrySpec>,
362 #[serde(default)]
363 pub priority: Option<TriggerPriorityField>,
364 #[serde(default)]
365 pub budget: Option<TriggerBudgetSpec>,
366 #[serde(default)]
367 pub concurrency: Option<TriggerConcurrencyManifestSpec>,
368 #[serde(default)]
369 pub throttle: Option<TriggerThrottleManifestSpec>,
370 #[serde(default)]
371 pub rate_limit: Option<TriggerRateLimitManifestSpec>,
372 #[serde(default)]
373 pub debounce: Option<TriggerDebounceManifestSpec>,
374 #[serde(default)]
375 pub singleton: Option<TriggerSingletonManifestSpec>,
376 #[serde(default)]
377 pub batch: Option<TriggerBatchManifestSpec>,
378 #[serde(default)]
379 pub window: Option<TriggerStreamWindowManifestSpec>,
380 #[serde(default)]
381 pub secrets: BTreeMap<String, String>,
382 #[serde(default)]
383 pub filter: Option<String>,
384 #[serde(flatten, default)]
385 pub kind_specific: BTreeMap<String, toml::Value>,
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
389#[serde(rename_all = "kebab-case")]
390pub enum TriggerKind {
391 Webhook,
392 Cron,
393 Poll,
394 Stream,
395 Predicate,
396 A2aPush,
397}
398
399#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
400pub struct TriggerMatchExpr {
401 #[serde(default)]
402 pub events: Vec<String>,
403 #[serde(flatten, default)]
404 pub extra: BTreeMap<String, toml::Value>,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
408pub struct TriggerRetrySpec {
409 #[serde(default)]
410 pub max: u32,
411 #[serde(default)]
412 pub backoff: TriggerRetryBackoff,
413 #[serde(default = "default_trigger_retention_days")]
414 pub retention_days: u32,
415}
416
417#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
418#[serde(rename_all = "kebab-case")]
419pub enum TriggerRetryBackoff {
420 #[default]
421 Immediate,
422 Svix,
423}
424
425pub(crate) fn default_trigger_retention_days() -> u32 {
426 harn_vm::DEFAULT_INBOX_RETENTION_DAYS
427}
428
429impl Default for TriggerRetrySpec {
430 fn default() -> Self {
431 Self {
432 max: 0,
433 backoff: TriggerRetryBackoff::default(),
434 retention_days: default_trigger_retention_days(),
435 }
436 }
437}
438
439#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
440#[serde(rename_all = "lowercase")]
441pub enum TriggerDispatchPriority {
442 High,
443 #[default]
444 Normal,
445 Low,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449#[serde(untagged)]
450pub enum TriggerPriorityField {
451 Dispatch(TriggerDispatchPriority),
452 Flow(TriggerPriorityManifestSpec),
453}
454
455#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
456pub struct TriggerBudgetSpec {
457 #[serde(default)]
458 pub max_cost_usd: Option<f64>,
459 #[serde(default, alias = "tokens_max")]
460 pub max_tokens: Option<u64>,
461 #[serde(default)]
462 pub daily_cost_usd: Option<f64>,
463 #[serde(default)]
464 pub hourly_cost_usd: Option<f64>,
465 #[serde(default)]
466 pub max_autonomous_decisions_per_hour: Option<u64>,
467 #[serde(default)]
468 pub max_autonomous_decisions_per_day: Option<u64>,
469 #[serde(default)]
470 pub max_concurrent: Option<u32>,
471 #[serde(default)]
472 pub on_budget_exhausted: harn_vm::TriggerBudgetExhaustionStrategy,
473}
474
475#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
476pub struct TriggerWhenBudgetSpec {
477 #[serde(default)]
478 pub max_cost_usd: Option<f64>,
479 #[serde(default)]
480 pub tokens_max: Option<u64>,
481 #[serde(default)]
482 pub timeout: Option<String>,
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
486pub struct TriggerConcurrencyManifestSpec {
487 #[serde(default)]
488 pub key: Option<String>,
489 pub max: u32,
490}
491
492#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
493pub struct TriggerThrottleManifestSpec {
494 #[serde(default)]
495 pub key: Option<String>,
496 pub period: String,
497 pub max: u32,
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct TriggerRateLimitManifestSpec {
502 #[serde(default)]
503 pub key: Option<String>,
504 pub period: String,
505 pub max: u32,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
509pub struct TriggerDebounceManifestSpec {
510 pub key: String,
511 pub period: String,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
515pub struct TriggerSingletonManifestSpec {
516 #[serde(default)]
517 pub key: Option<String>,
518}
519
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
521pub struct TriggerBatchManifestSpec {
522 #[serde(default)]
523 pub key: Option<String>,
524 pub size: u32,
525 pub timeout: String,
526}
527
528#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
529pub struct TriggerPriorityManifestSpec {
530 pub key: String,
531 #[serde(default)]
532 pub order: Vec<String>,
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(rename_all = "kebab-case")]
537pub enum TriggerStreamWindowMode {
538 Tumbling,
539 Sliding,
540 Session,
541}
542
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
544pub struct TriggerStreamWindowManifestSpec {
545 pub mode: TriggerStreamWindowMode,
546 #[serde(default)]
547 pub key: Option<String>,
548 #[serde(default)]
549 pub size: Option<String>,
550 #[serde(default)]
551 pub every: Option<String>,
552 #[serde(default)]
553 pub gap: Option<String>,
554 #[serde(default)]
555 pub max_items: Option<u32>,
556}
557
558#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
559pub struct TriggerDlqAlertManifestSpec {
560 #[serde(default)]
561 pub destinations: Vec<TriggerDlqAlertDestination>,
562 #[serde(default)]
563 pub threshold: TriggerDlqAlertThreshold,
564}
565
566#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
567pub struct TriggerDlqAlertThreshold {
568 #[serde(default, alias = "entries-in-1h")]
569 pub entries_in_1h: Option<u32>,
570 #[serde(default, alias = "percent-of-dispatches")]
571 pub percent_of_dispatches: Option<f64>,
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
575#[serde(tag = "kind", rename_all = "snake_case")]
576pub enum TriggerDlqAlertDestination {
577 Slack {
578 channel: String,
579 #[serde(default)]
580 webhook_url_env: Option<String>,
581 },
582 Email {
583 address: String,
584 },
585 Webhook {
586 url: String,
587 #[serde(default)]
588 headers: BTreeMap<String, String>,
589 },
590}
591
592impl TriggerDlqAlertDestination {
593 pub fn label(&self) -> String {
594 match self {
595 Self::Slack { channel, .. } => format!("slack:{channel}"),
596 Self::Email { address } => format!("email:{address}"),
597 Self::Webhook { url, .. } => format!("webhook:{url}"),
598 }
599 }
600}
601
602#[derive(Debug, Clone, PartialEq, Eq)]
603pub enum TriggerHandlerUri {
604 Local(TriggerFunctionRef),
605 A2a {
606 target: String,
607 allow_cleartext: bool,
608 },
609 Worker {
610 queue: String,
611 },
612 Persona {
613 name: String,
614 },
615 EvalPack {
616 target: String,
617 },
618}
619
620#[derive(Debug, Clone, PartialEq, Eq)]
621pub struct TriggerFunctionRef {
622 pub raw: String,
623 pub module_name: Option<String>,
624 pub function_name: String,
625}
626
627#[derive(Debug, Default, Clone, Deserialize)]
629#[allow(dead_code)] pub struct SkillsConfig {
631 #[serde(default)]
635 pub paths: Vec<String>,
636 #[serde(default)]
641 pub lookup_order: Vec<String>,
642 #[serde(default)]
644 pub disable: Vec<String>,
645 #[serde(default)]
648 pub signer_registry_url: Option<String>,
649 #[serde(default)]
653 pub defaults: SkillDefaults,
654}
655
656#[derive(Debug, Default, Clone, Deserialize)]
657#[allow(dead_code)] pub struct SkillDefaults {
659 #[serde(default)]
660 pub tool_search: Option<String>,
661 #[serde(default)]
662 pub always_loaded: Vec<String>,
663}
664
665#[derive(Debug, Default, Clone, Deserialize)]
667pub struct SkillTables {
668 #[serde(default, rename = "source")]
669 pub sources: Vec<SkillSourceEntry>,
670}
671
672#[derive(Debug, Clone, Deserialize)]
676#[serde(tag = "type", rename_all = "lowercase")]
677#[allow(dead_code)] pub enum SkillSourceEntry {
679 Fs {
680 path: String,
681 #[serde(default)]
682 namespace: Option<String>,
683 },
684 Git {
685 url: String,
686 #[serde(default)]
687 tag: Option<String>,
688 #[serde(default)]
689 namespace: Option<String>,
690 },
691 Registry {
692 #[serde(default)]
693 url: Option<String>,
694 #[serde(default)]
695 name: Option<String>,
696 },
697}
698
699#[derive(Debug, Default, Clone, Deserialize)]
700pub struct WorkspaceConfig {
701 #[serde(default)]
704 pub pipelines: Vec<String>,
705}
706
707#[derive(Debug, Default, Clone, Deserialize)]
708pub struct PackageRegistryConfig {
709 #[serde(default)]
711 pub url: Option<String>,
712}
713
714#[derive(Debug, Clone, Deserialize)]
715pub struct McpServerConfig {
716 pub name: String,
717 #[serde(default)]
718 pub transport: Option<String>,
719 #[serde(default)]
720 pub command: String,
721 #[serde(default)]
722 pub args: Vec<String>,
723 #[serde(default)]
724 pub env: HashMap<String, String>,
725 #[serde(default)]
726 pub url: String,
727 #[serde(default)]
728 pub auth_token: Option<String>,
729 #[serde(default)]
730 pub token_exchange: Option<harn_vm::mcp_oauth::McpTokenExchangeConfig>,
731 #[serde(default)]
732 pub auth: Option<McpAuthConfig>,
733 #[serde(default)]
734 pub client_id: Option<String>,
735 #[serde(default)]
736 pub client_secret: Option<String>,
737 #[serde(default)]
738 pub scopes: Option<String>,
739 #[serde(default)]
740 pub protocol_version: Option<String>,
741 #[serde(default)]
742 pub proxy_server_name: Option<String>,
743 #[serde(default)]
747 pub lazy: bool,
748 #[serde(default)]
752 pub card: Option<String>,
753 #[serde(default, alias = "keep-alive-ms", alias = "keep_alive")]
757 pub keep_alive_ms: Option<u64>,
758}
759
760#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
761pub struct McpAuthConfig {
762 #[serde(default)]
763 pub mode: Option<harn_vm::mcp_auth::OAuthClientAuthMode>,
764 #[serde(default, alias = "client-id")]
765 pub client_id: Option<String>,
766 #[serde(
767 default,
768 alias = "client_secret_id",
769 alias = "client-secret-id",
770 alias = "client_secret_ref",
771 alias = "client-secret-ref"
772 )]
773 pub client_secret_id: Option<String>,
774 #[serde(
775 default,
776 alias = "secret_id",
777 alias = "secret-id",
778 alias = "token-secret-id"
779 )]
780 pub secret_id: Option<String>,
781 #[serde(default, alias = "scope")]
782 pub scopes: Option<String>,
783 #[serde(default, alias = "token_auth_method", alias = "token-auth-method")]
784 pub token_endpoint_auth_method: Option<String>,
785}
786
787#[derive(Debug, Clone, Deserialize)]
788#[allow(dead_code)] pub struct PackageInfo {
790 pub name: Option<String>,
791 pub version: Option<String>,
792 #[serde(default)]
793 pub evals: Vec<String>,
794 #[serde(default)]
795 pub description: Option<String>,
796 #[serde(default)]
797 pub license: Option<String>,
798 #[serde(default)]
799 pub repository: Option<String>,
800 #[serde(default, alias = "harn_version", alias = "harn_version_range")]
801 pub harn: Option<String>,
802 #[serde(default)]
803 pub docs_url: Option<String>,
804 #[serde(default)]
805 pub provenance: Option<String>,
806 #[serde(default)]
808 pub publisher: Option<String>,
809 #[serde(default)]
811 pub contact: Option<String>,
812 #[serde(default)]
815 pub created: Option<String>,
816 #[serde(default)]
817 pub permissions: Vec<String>,
818 #[serde(default, alias = "host-requirements")]
819 pub host_requirements: Vec<String>,
820 #[serde(default)]
821 pub tools: Vec<PackageToolExport>,
822 #[serde(default)]
823 pub skills: Vec<PackageSkillExport>,
824}
825
826#[derive(Debug, Clone, Deserialize, PartialEq)]
827pub struct PackageToolExport {
828 pub name: String,
829 pub module: String,
830 #[serde(default = "default_package_tool_symbol")]
831 pub symbol: String,
832 #[serde(default)]
833 pub description: Option<String>,
834 #[serde(default)]
835 pub permissions: Vec<String>,
836 #[serde(default, alias = "host-requirements")]
837 pub host_requirements: Vec<String>,
838 #[serde(default, alias = "input-schema")]
839 pub input_schema: Option<toml::Value>,
840 #[serde(default, alias = "output-schema")]
841 pub output_schema: Option<toml::Value>,
842 #[serde(default)]
843 pub annotations: BTreeMap<String, toml::Value>,
844}
845
846pub(crate) fn default_package_tool_symbol() -> String {
847 "tools".to_string()
848}
849
850#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
851pub struct PackageSkillExport {
852 pub name: String,
853 pub path: String,
854 #[serde(default)]
855 pub description: Option<String>,
856 #[serde(default)]
857 pub permissions: Vec<String>,
858 #[serde(default, alias = "host-requirements")]
859 pub host_requirements: Vec<String>,
860}
861
862#[derive(Debug, Clone, Deserialize)]
863#[serde(untagged)]
864pub enum Dependency {
865 Table(Box<DepTable>),
866 Path(String),
867}
868
869#[derive(Debug, Clone, Default, Deserialize)]
870pub struct DepTable {
871 pub git: Option<String>,
872 #[serde(default, alias = "archive-url", alias = "archive_url")]
873 pub archive: Option<String>,
874 pub tag: Option<String>,
875 pub rev: Option<String>,
876 pub branch: Option<String>,
877 pub version: Option<String>,
878 pub path: Option<String>,
879 pub package: Option<String>,
880 #[serde(default)]
881 pub checksum: Option<String>,
882 #[serde(default)]
887 pub registry: Option<String>,
888 #[serde(default, alias = "registry-name")]
891 pub registry_name: Option<String>,
892 #[serde(default, alias = "registry-version")]
894 pub registry_version: Option<String>,
895 #[serde(default, alias = "registry-commit")]
897 pub registry_commit: Option<String>,
898 #[serde(default, alias = "registry-provenance")]
900 pub registry_provenance: Option<String>,
901}
902
903impl Dependency {
904 pub(crate) fn git_url(&self) -> Option<&str> {
905 match self {
906 Dependency::Table(t) => t.git.as_deref(),
907 Dependency::Path(_) => None,
908 }
909 }
910
911 pub(crate) fn archive_url(&self) -> Option<&str> {
912 match self {
913 Dependency::Table(t) => t.archive.as_deref(),
914 Dependency::Path(_) => None,
915 }
916 }
917
918 pub(crate) fn rev(&self) -> Option<&str> {
919 match self {
920 Dependency::Table(t) => t.rev.as_deref(),
921 Dependency::Path(_) => None,
922 }
923 }
924
925 pub(crate) fn tag(&self) -> Option<&str> {
926 match self {
927 Dependency::Table(t) => t.tag.as_deref(),
928 Dependency::Path(_) => None,
929 }
930 }
931
932 pub(crate) fn branch(&self) -> Option<&str> {
933 match self {
934 Dependency::Table(t) => t.branch.as_deref(),
935 Dependency::Path(_) => None,
936 }
937 }
938
939 pub(crate) fn version(&self) -> Option<&str> {
940 match self {
941 Dependency::Table(t) => t.version.as_deref(),
942 Dependency::Path(_) => None,
943 }
944 }
945
946 pub(crate) fn requires_git(&self) -> bool {
947 self.git_url().is_some()
948 }
949
950 pub(crate) fn local_path(&self) -> Option<&str> {
951 match self {
952 Dependency::Table(t) => t.path.as_deref(),
953 Dependency::Path(p) => Some(p.as_str()),
954 }
955 }
956}
957
958pub(crate) fn validate_package_alias(alias: &str) -> Result<(), PackageError> {
959 if harn_modules::package_snapshot::is_valid_package_name(alias) {
960 Ok(())
961 } else {
962 Err(PackageError::Validation(format!(
963 "invalid dependency alias {alias:?}; use ASCII letters, numbers, '.', '_' or '-'"
964 )))
965 }
966}
967
968pub(crate) fn toml_string_literal(value: &str) -> Result<String, PackageError> {
969 use std::fmt::Write as _;
970
971 let mut encoded = String::with_capacity(value.len() + 2);
972 encoded.push('"');
973 for ch in value.chars() {
974 match ch {
975 '\u{08}' => encoded.push_str("\\b"),
976 '\t' => encoded.push_str("\\t"),
977 '\n' => encoded.push_str("\\n"),
978 '\u{0C}' => encoded.push_str("\\f"),
979 '\r' => encoded.push_str("\\r"),
980 '"' => encoded.push_str("\\\""),
981 '\\' => encoded.push_str("\\\\"),
982 ch if ch <= '\u{1F}' || ch == '\u{7F}' => {
983 write!(&mut encoded, "\\u{:04X}", ch as u32).map_err(|error| {
984 PackageError::Manifest(format!("failed to encode TOML string: {error}"))
985 })?;
986 }
987 ch => encoded.push(ch),
988 }
989 }
990 encoded.push('"');
991 Ok(encoded)
992}
993#[derive(Debug, Default, Clone)]
994pub struct RuntimeExtensions {
995 pub root_manifest: Option<Manifest>,
996 pub root_manifest_path: Option<PathBuf>,
997 pub root_manifest_dir: Option<PathBuf>,
998 pub(crate) runtime_personas: Vec<ResolvedRuntimePersona>,
999 pub llm: Option<harn_vm::llm_config::ProvidersConfig>,
1000 pub capabilities: Option<harn_vm::llm::capabilities::CapabilitiesFile>,
1001 pub hooks: Vec<ResolvedHookConfig>,
1002 pub triggers: Vec<ResolvedTriggerConfig>,
1003 pub handoff_routes: Vec<harn_vm::HandoffRouteConfig>,
1004 pub provider_connectors: Vec<ResolvedProviderConnectorConfig>,
1005}
1006
1007#[derive(Debug, Clone, Deserialize)]
1008pub struct ProviderConnectorManifest {
1009 #[serde(default)]
1010 pub harn: Option<String>,
1011 #[serde(default)]
1012 pub rust: Option<String>,
1013}
1014
1015#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
1016pub struct ProviderOAuthManifest {
1017 #[serde(default, alias = "auth_url", alias = "authorization-endpoint")]
1018 pub authorization_endpoint: Option<String>,
1019 #[serde(default, alias = "token_url", alias = "token-endpoint")]
1020 pub token_endpoint: Option<String>,
1021 #[serde(default, alias = "registration_url", alias = "registration-endpoint")]
1022 pub registration_endpoint: Option<String>,
1023 #[serde(default)]
1024 pub resource: Option<String>,
1025 #[serde(default, alias = "scope")]
1026 pub scopes: Option<String>,
1027 #[serde(default, alias = "client-id")]
1028 pub client_id: Option<String>,
1029 #[serde(default, alias = "client-secret")]
1030 pub client_secret: Option<String>,
1031 #[serde(default, alias = "token_auth_method", alias = "token-auth-method")]
1032 pub token_endpoint_auth_method: Option<String>,
1033}
1034
1035#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
1036pub struct ConnectorCapabilities {
1037 pub webhook: bool,
1038 pub oauth: bool,
1039 pub rate_limit: bool,
1040 pub pagination: bool,
1041 pub graphql: bool,
1042 pub streaming: bool,
1043}
1044
1045impl ConnectorCapabilities {
1046 pub const FEATURES: [&'static str; 6] = [
1047 "webhook",
1048 "oauth",
1049 "rate_limit",
1050 "pagination",
1051 "graphql",
1052 "streaming",
1053 ];
1054
1055 fn enable(&mut self, feature: &str) -> Result<(), String> {
1056 match normalize_connector_capability(feature).as_str() {
1057 "webhook" => self.webhook = true,
1058 "oauth" => self.oauth = true,
1059 "rate_limit" => self.rate_limit = true,
1060 "pagination" => self.pagination = true,
1061 "graphql" => self.graphql = true,
1062 "streaming" => self.streaming = true,
1063 other => {
1064 return Err(format!(
1065 "unknown connector capability '{feature}' (normalized as '{other}')"
1066 ));
1067 }
1068 }
1069 Ok(())
1070 }
1071}
1072
1073#[derive(Debug, Default, Deserialize)]
1074struct ConnectorCapabilitiesTable {
1075 #[serde(default)]
1076 webhook: bool,
1077 #[serde(default)]
1078 oauth: bool,
1079 #[serde(default, alias = "rate-limit")]
1080 rate_limit: bool,
1081 #[serde(default)]
1082 pagination: bool,
1083 #[serde(default)]
1084 graphql: bool,
1085 #[serde(default)]
1086 streaming: bool,
1087}
1088
1089impl From<ConnectorCapabilitiesTable> for ConnectorCapabilities {
1090 fn from(value: ConnectorCapabilitiesTable) -> Self {
1091 Self {
1092 webhook: value.webhook,
1093 oauth: value.oauth,
1094 rate_limit: value.rate_limit,
1095 pagination: value.pagination,
1096 graphql: value.graphql,
1097 streaming: value.streaming,
1098 }
1099 }
1100}
1101
1102impl<'de> Deserialize<'de> for ConnectorCapabilities {
1103 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1104 where
1105 D: serde::Deserializer<'de>,
1106 {
1107 #[derive(Deserialize)]
1108 #[serde(untagged)]
1109 enum RawConnectorCapabilities {
1110 List(Vec<String>),
1111 Table(ConnectorCapabilitiesTable),
1112 }
1113
1114 match RawConnectorCapabilities::deserialize(deserializer)? {
1115 RawConnectorCapabilities::List(features) => {
1116 let mut capabilities = ConnectorCapabilities::default();
1117 for feature in features {
1118 capabilities
1119 .enable(&feature)
1120 .map_err(serde::de::Error::custom)?;
1121 }
1122 Ok(capabilities)
1123 }
1124 RawConnectorCapabilities::Table(table) => Ok(table.into()),
1125 }
1126 }
1127}
1128
1129pub fn normalize_connector_capability(feature: &str) -> String {
1130 feature.trim().to_lowercase().replace('-', "_")
1131}
1132
1133#[derive(Debug, Clone, Default, Deserialize)]
1134pub struct ConnectorContractConfig {
1135 #[serde(default)]
1136 pub version: Option<u32>,
1137 #[serde(default)]
1138 pub fixtures: Vec<ConnectorContractFixture>,
1139}
1140
1141#[derive(Debug, Clone, Deserialize)]
1142pub struct ConnectorContractFixture {
1143 pub provider: harn_vm::ProviderId,
1144 #[serde(default)]
1145 pub name: Option<String>,
1146 #[serde(default)]
1147 pub kind: Option<String>,
1148 #[serde(default)]
1149 pub headers: BTreeMap<String, String>,
1150 #[serde(default)]
1151 pub query: BTreeMap<String, String>,
1152 #[serde(default)]
1153 pub metadata: Option<toml::Value>,
1154 #[serde(default)]
1155 pub body: Option<String>,
1156 #[serde(default)]
1157 pub body_json: Option<toml::Value>,
1158 #[serde(default)]
1159 pub expect_type: Option<String>,
1160 #[serde(default)]
1161 pub expect_kind: Option<String>,
1162 #[serde(default)]
1163 pub expect_dedupe_key: Option<String>,
1164 #[serde(default)]
1165 pub expect_signature_state: Option<String>,
1166 #[serde(default)]
1167 pub expect_payload_contains: Option<toml::Value>,
1168 #[serde(default)]
1169 pub expect_response_status: Option<u16>,
1170 #[serde(default)]
1171 pub expect_response_body: Option<toml::Value>,
1172 #[serde(default)]
1173 pub expect_event_count: Option<usize>,
1174 #[serde(default)]
1175 pub expect_error_contains: Option<String>,
1176}
1177
1178#[derive(Debug, Clone, PartialEq, Eq)]
1179pub enum ResolvedProviderConnectorKind {
1180 Harn { module: String },
1181 RustBuiltin,
1182 Invalid(String),
1183}
1184
1185#[derive(Debug, Clone)]
1186pub struct ResolvedHookConfig {
1187 pub event: harn_vm::orchestration::HookEvent,
1188 pub pattern: String,
1189 pub handler: String,
1190 pub manifest_dir: PathBuf,
1191 pub package_name: Option<String>,
1192 pub exports: HashMap<String, String>,
1193}
1194
1195#[derive(Debug, Clone)]
1196pub struct ResolvedTriggerConfig {
1197 pub id: String,
1198 pub kind: TriggerKind,
1199 pub provider: harn_vm::ProviderId,
1200 pub autonomy_tier: harn_vm::AutonomyTier,
1201 pub match_: TriggerMatchExpr,
1202 pub when: Option<String>,
1203 pub when_budget: Option<TriggerWhenBudgetSpec>,
1204 pub handler: String,
1205 pub dedupe_key: Option<String>,
1206 pub retry: TriggerRetrySpec,
1207 pub dispatch_priority: TriggerDispatchPriority,
1208 pub budget: TriggerBudgetSpec,
1209 pub concurrency: Option<TriggerConcurrencyManifestSpec>,
1210 pub throttle: Option<TriggerThrottleManifestSpec>,
1211 pub rate_limit: Option<TriggerRateLimitManifestSpec>,
1212 pub debounce: Option<TriggerDebounceManifestSpec>,
1213 pub singleton: Option<TriggerSingletonManifestSpec>,
1214 pub batch: Option<TriggerBatchManifestSpec>,
1215 pub window: Option<TriggerStreamWindowManifestSpec>,
1216 pub priority_flow: Option<TriggerPriorityManifestSpec>,
1217 pub secrets: BTreeMap<String, String>,
1218 pub filter: Option<String>,
1219 pub kind_specific: BTreeMap<String, toml::Value>,
1220 pub manifest_dir: PathBuf,
1221 pub manifest_path: PathBuf,
1222 pub package_name: Option<String>,
1223 pub exports: HashMap<String, String>,
1224 pub execution_guard: Option<Arc<harn_modules::package_execution::PackageExecutionGuard>>,
1225 pub table_index: usize,
1226 pub shape_error: Option<String>,
1227}
1228
1229#[derive(Debug, Clone)]
1230#[allow(dead_code)] pub struct CollectedManifestTrigger {
1232 pub config: ResolvedTriggerConfig,
1233 pub handler: CollectedTriggerHandler,
1234 pub when: Option<CollectedTriggerPredicate>,
1235 pub flow_control: harn_vm::TriggerFlowControlConfig,
1236}
1237
1238#[derive(Debug, Clone)]
1239#[allow(dead_code)] pub enum CollectedTriggerHandler {
1241 Local {
1242 reference: TriggerFunctionRef,
1243 callable: harn_vm::VmCallable,
1244 },
1245 A2a {
1246 target: String,
1247 allow_cleartext: bool,
1248 },
1249 Worker {
1250 queue: String,
1251 },
1252 Persona {
1253 binding: harn_vm::PersonaRuntimeBinding,
1254 callable: harn_vm::VmCallable,
1255 },
1256 EvalPack {
1257 target: String,
1258 manifest: Box<harn_vm::orchestration::EvalPackManifest>,
1259 ledger_options: Option<serde_json::Value>,
1260 },
1261}
1262#[derive(Debug, Clone)]
1263#[allow(dead_code)] pub struct CollectedTriggerPredicate {
1265 pub reference: TriggerFunctionRef,
1266 pub callable: harn_vm::VmCallable,
1267}
1268
1269pub(crate) type ManifestModuleCacheKey = (PathBuf, Option<String>, Option<String>);
1270pub(crate) type ManifestModuleExports = BTreeMap<String, Arc<harn_vm::VmClosure>>;
1271
1272static MANIFEST_PROVIDER_SCHEMA_LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
1273
1274pub(crate) async fn lock_manifest_provider_schemas() -> tokio::sync::MutexGuard<'static, ()> {
1275 MANIFEST_PROVIDER_SCHEMA_LOCK
1276 .get_or_init(|| tokio::sync::Mutex::new(()))
1277 .lock()
1278 .await
1279}
1280
1281fn llm_manifest_diagnostics(content: &str) -> Vec<harn_vm::llm_config::ProviderConfigDiagnostic> {
1282 let Ok(value) = toml::from_str::<toml::Value>(content) else {
1283 return Vec::new();
1284 };
1285 let Some(llm) = value.get("llm") else {
1286 return Vec::new();
1287 };
1288 let Ok(llm_src) = toml::to_string(llm) else {
1289 return Vec::new();
1290 };
1291 let Ok(parsed) = harn_vm::llm_config::parse_config_toml_with_diagnostics(&llm_src) else {
1292 return Vec::new();
1293 };
1294 parsed
1295 .diagnostics
1296 .into_iter()
1297 .map(|mut diagnostic| {
1298 if !diagnostic.path.is_empty() {
1299 diagnostic.path = format!("llm.{}", diagnostic.path);
1300 }
1301 diagnostic
1302 })
1303 .collect()
1304}
1305
1306pub(crate) fn read_manifest_from_path(path: &Path) -> Result<Manifest, PackageError> {
1307 let content = fs::read_to_string(path).map_err(|error| {
1308 if error.kind() == std::io::ErrorKind::NotFound {
1309 PackageError::Manifest(format!(
1310 "No {} found in {}.",
1311 MANIFEST,
1312 path.parent().unwrap_or_else(|| Path::new(".")).display()
1313 ))
1314 } else {
1315 PackageError::Manifest(format!("failed to read {}: {error}", path.display()))
1316 }
1317 })?;
1318 let manifest = toml::from_str::<Manifest>(&content).map_err(|error| {
1319 PackageError::Manifest(format!("failed to parse {}: {error}", path.display()))
1320 })?;
1321 for diagnostic in llm_manifest_diagnostics(&content) {
1322 eprintln!("[llm_config] warning in {}: {diagnostic}", path.display());
1323 }
1324 Ok(manifest)
1325}
1326
1327pub fn load_workspace_config(anchor: Option<&Path>) -> Option<(WorkspaceConfig, PathBuf)> {
1331 let anchor = anchor
1332 .map(Path::to_path_buf)
1333 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1334 let (manifest, dir) = nearest_manifest_or_warn(&anchor)?;
1335 Some((manifest.workspace, dir))
1336}
1337
1338pub fn load_package_eval_pack_paths(anchor: Option<&Path>) -> Result<Vec<PathBuf>, PackageError> {
1339 let anchor = anchor
1340 .map(Path::to_path_buf)
1341 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1342 let Some((manifest, dir)) = load_nearest_manifest(&anchor).into_result()? else {
1343 return Err(PackageError::Manifest(
1344 "no harn.toml found for package eval discovery".to_string(),
1345 ));
1346 };
1347
1348 let ctx = ManifestContext { manifest, dir };
1349 let mut paths = eval_pack_paths_from_manifest(&ctx.manifest, &ctx.dir)?;
1350 paths.extend(installed_package_eval_pack_paths(&ctx)?);
1351 paths.sort();
1352 paths.dedup();
1353 if paths.is_empty() {
1354 return Err(PackageError::Manifest(
1355 "package declares no eval packs; add [package].evals, harn.eval.toml, or install a dependency that ships eval packs".to_string(),
1356 ));
1357 }
1358 for path in &paths {
1359 if !path.is_file() {
1360 return Err(PackageError::Manifest(format!(
1361 "eval pack does not exist: {}",
1362 path.display()
1363 )));
1364 }
1365 }
1366 Ok(paths)
1367}
1368
1369fn eval_pack_paths_from_manifest(
1370 manifest: &Manifest,
1371 manifest_dir: &Path,
1372) -> Result<Vec<PathBuf>, PackageError> {
1373 let declared = manifest
1374 .package
1375 .as_ref()
1376 .map(|package| package.evals.clone())
1377 .unwrap_or_default();
1378 let paths = if declared.is_empty() {
1379 let default_pack = manifest_dir.join("harn.eval.toml");
1380 if default_pack.is_file() {
1381 vec![default_pack]
1382 } else {
1383 Vec::new()
1384 }
1385 } else {
1386 declared
1387 .iter()
1388 .map(|entry| {
1389 let path = PathBuf::from(entry);
1390 if path.is_absolute() {
1391 path
1392 } else {
1393 manifest_dir.join(path)
1394 }
1395 })
1396 .collect()
1397 };
1398 for path in &paths {
1399 if !path.is_file() {
1400 return Err(PackageError::Manifest(format!(
1401 "eval pack does not exist: {}",
1402 path.display()
1403 )));
1404 }
1405 }
1406 Ok(paths)
1407}
1408
1409fn installed_package_eval_pack_paths(ctx: &ManifestContext) -> Result<Vec<PathBuf>, PackageError> {
1410 let Some(snapshot) = dependency_package_snapshot(&ctx.manifest, &ctx.dir)? else {
1411 return Ok(Vec::new());
1412 };
1413 let lock = LockFile::load(snapshot.lock_path())?.ok_or_else(|| {
1414 PackageError::Lockfile(format!(
1415 "published package generation is missing {}",
1416 snapshot.lock_path().display()
1417 ))
1418 })?;
1419 let mut paths = Vec::new();
1420 let packages_dir = snapshot.packages_root();
1421 for entry in &lock.packages {
1422 validate_package_alias(&entry.name)?;
1423 let package_dir = packages_dir.join(&entry.name);
1424 if package_dir.is_dir() {
1425 if let Some(manifest) = read_package_manifest_from_dir(&package_dir)? {
1426 paths.extend(eval_pack_paths_from_manifest(&manifest, &package_dir)?);
1427 }
1428 continue;
1429 }
1430
1431 let package_file = packages_dir.join(format!("{}.harn", entry.name));
1432 if package_file.is_file() {
1433 continue;
1434 }
1435
1436 return Err(PackageError::Manifest(format!(
1437 "installed package {} is missing under {}; run `harn install`",
1438 entry.name,
1439 packages_dir.display()
1440 )));
1441 }
1442 Ok(paths)
1443}
1444
1445#[derive(Debug, Clone)]
1446pub(crate) struct ManifestContext {
1447 pub(crate) manifest: Manifest,
1448 pub(crate) dir: PathBuf,
1449}
1450
1451impl ManifestContext {
1452 pub(crate) fn manifest_path(&self) -> PathBuf {
1453 self.dir.join(MANIFEST)
1454 }
1455
1456 pub(crate) fn lock_path(&self) -> PathBuf {
1457 self.dir.join(LOCK_FILE)
1458 }
1459}
1460
1461#[cfg(test)]
1462#[path = "manifest_tests.rs"]
1463mod tests;