1use super::errors::PackageError;
2use super::*;
3mod check_config;
4pub use check_config::{CheckConfig, PreflightSeverity};
5pub use harn_modules::personas::{
6 PersonaAutonomyTier, PersonaManifestEntry, PersonaStageDecl, PersonaStageExit,
7 PersonaValidationError, ResolvedPersonaManifest,
8};
9
10#[derive(Debug, Clone, Deserialize)]
11pub struct Manifest {
12 pub package: Option<PackageInfo>,
13 #[serde(default)]
14 pub dependencies: HashMap<String, Dependency>,
15 #[serde(default)]
16 pub mcp: Vec<McpServerConfig>,
17 #[serde(default)]
18 pub check: CheckConfig,
19 #[serde(default)]
20 pub workspace: WorkspaceConfig,
21 #[serde(default)]
25 pub registry: PackageRegistryConfig,
26 #[serde(default)]
29 pub skills: SkillsConfig,
30 #[serde(default)]
33 pub skill: SkillTables,
34 #[serde(default)]
42 pub capabilities: Option<harn_vm::llm::capabilities::CapabilitiesFile>,
43 #[serde(default)]
47 pub exports: HashMap<String, String>,
48 #[serde(default)]
53 pub llm: harn_vm::llm_config::ProvidersConfig,
54 #[serde(default)]
59 pub hooks: Vec<HookConfig>,
60 #[serde(default)]
65 pub triggers: Vec<TriggerManifestEntry>,
66 #[serde(default)]
70 pub handoff_routes: Vec<harn_vm::HandoffRouteConfig>,
71 #[serde(default)]
75 pub providers: Vec<ProviderManifestEntry>,
76 #[serde(default)]
80 pub personas: Vec<PersonaManifestEntry>,
81 #[serde(default, alias = "connector-contract")]
84 pub connector_contract: ConnectorContractConfig,
85 #[serde(default)]
88 pub orchestrator: OrchestratorConfig,
89 #[serde(default)]
93 pub rules: RulesConfig,
94 #[serde(default)]
104 pub contributes: Vec<ContributionEntry>,
105}
106
107#[derive(Debug, Clone, Deserialize, Serialize)]
122pub struct ContributionEntry {
123 pub kind: String,
128 pub id: String,
130 #[serde(default)]
131 pub title: Option<String>,
132 #[serde(default)]
135 pub when: Option<String>,
136 #[serde(default)]
139 pub scopes: Vec<String>,
140 #[serde(default)]
144 pub platforms: Vec<String>,
145 #[serde(flatten)]
147 pub config: BTreeMap<String, toml::Value>,
148}
149
150impl ContributionEntry {
151 pub fn has_namespaced_kind(&self) -> bool {
155 let mut segments = 0usize;
156 for segment in self.kind.split('.') {
157 if segment.is_empty()
158 || !segment
159 .chars()
160 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
161 || !segment.starts_with(|c: char| c.is_ascii_lowercase())
162 {
163 return false;
164 }
165 segments += 1;
166 }
167 segments >= 2
168 }
169}
170
171#[derive(Debug, Clone, Default, Deserialize)]
183pub struct RulesConfig {
184 #[serde(default, alias = "rule-dirs", alias = "ruleDirs")]
186 pub rule_dirs: Vec<String>,
187 #[serde(default, alias = "util-dirs", alias = "utilDirs")]
189 pub util_dirs: Vec<String>,
190 #[serde(default, alias = "test-configs", alias = "testConfigs")]
192 pub test_configs: Vec<String>,
193 #[serde(
195 default,
196 alias = "native-rule-dirs",
197 alias = "nativeRuleDirs",
198 alias = "native_rule_dirs"
199 )]
200 pub native_rule_dirs: Vec<String>,
201}
202
203#[derive(Debug, Clone, Default, Deserialize)]
204pub struct OrchestratorConfig {
205 #[serde(default, alias = "allowed-origins")]
206 pub allowed_origins: Vec<String>,
207 #[serde(default, alias = "max-body-bytes")]
208 pub max_body_bytes: Option<usize>,
209 #[serde(default)]
210 pub budget: OrchestratorBudgetSpec,
211 #[serde(default)]
212 pub drain: OrchestratorDrainConfig,
213 #[serde(default)]
214 pub pumps: OrchestratorPumpConfig,
215}
216
217#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
218pub struct OrchestratorBudgetSpec {
219 #[serde(default)]
220 pub daily_cost_usd: Option<f64>,
221 #[serde(default)]
222 pub hourly_cost_usd: Option<f64>,
223}
224
225#[derive(Debug, Clone, Deserialize)]
226pub struct OrchestratorDrainConfig {
227 #[serde(default = "default_orchestrator_drain_max_items", alias = "max-items")]
228 pub max_items: usize,
229 #[serde(
230 default = "default_orchestrator_drain_deadline_seconds",
231 alias = "deadline-seconds"
232 )]
233 pub deadline_seconds: u64,
234}
235
236impl Default for OrchestratorDrainConfig {
237 fn default() -> Self {
238 Self {
239 max_items: default_orchestrator_drain_max_items(),
240 deadline_seconds: default_orchestrator_drain_deadline_seconds(),
241 }
242 }
243}
244
245pub(crate) fn default_orchestrator_drain_max_items() -> usize {
246 1024
247}
248
249pub(crate) fn default_orchestrator_drain_deadline_seconds() -> u64 {
250 30
251}
252
253#[derive(Debug, Clone, Deserialize)]
254pub struct OrchestratorPumpConfig {
255 #[serde(
256 default = "default_orchestrator_pump_max_outstanding",
257 alias = "max-outstanding"
258 )]
259 pub max_outstanding: usize,
260}
261
262impl Default for OrchestratorPumpConfig {
263 fn default() -> Self {
264 Self {
265 max_outstanding: default_orchestrator_pump_max_outstanding(),
266 }
267 }
268}
269
270pub(crate) fn default_orchestrator_pump_max_outstanding() -> usize {
271 64
272}
273
274#[derive(Debug, Clone, Deserialize)]
275pub struct HookConfig {
276 pub event: harn_vm::orchestration::HookEvent,
277 #[serde(default = "default_hook_pattern")]
278 pub pattern: String,
279 pub handler: String,
280}
281
282pub(crate) fn default_hook_pattern() -> String {
283 "*".to_string()
284}
285
286#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
287pub struct TriggerManifestEntry {
288 pub id: String,
289 #[serde(default)]
290 pub kind: Option<TriggerKind>,
291 #[serde(default)]
292 pub provider: Option<harn_vm::ProviderId>,
293 #[serde(default, alias = "tier")]
294 pub autonomy_tier: harn_vm::AutonomyTier,
295 #[serde(default, rename = "match")]
296 pub match_: Option<TriggerMatchExpr>,
297 #[serde(default)]
298 pub sources: Vec<TriggerSourceManifestEntry>,
299 #[serde(default)]
300 pub when: Option<String>,
301 #[serde(default)]
302 pub when_budget: Option<TriggerWhenBudgetSpec>,
303 pub handler: String,
304 #[serde(default)]
305 pub dedupe_key: Option<String>,
306 #[serde(default)]
307 pub retry: TriggerRetrySpec,
308 #[serde(default)]
309 pub priority: Option<TriggerPriorityField>,
310 #[serde(default)]
311 pub budget: TriggerBudgetSpec,
312 #[serde(default)]
313 pub concurrency: Option<TriggerConcurrencyManifestSpec>,
314 #[serde(default)]
315 pub throttle: Option<TriggerThrottleManifestSpec>,
316 #[serde(default)]
317 pub rate_limit: Option<TriggerRateLimitManifestSpec>,
318 #[serde(default)]
319 pub debounce: Option<TriggerDebounceManifestSpec>,
320 #[serde(default)]
321 pub singleton: Option<TriggerSingletonManifestSpec>,
322 #[serde(default)]
323 pub batch: Option<TriggerBatchManifestSpec>,
324 #[serde(default)]
325 pub window: Option<TriggerStreamWindowManifestSpec>,
326 #[serde(default, alias = "dlq-alerts")]
327 pub dlq_alerts: Vec<TriggerDlqAlertManifestSpec>,
328 #[serde(default)]
329 pub secrets: BTreeMap<String, String>,
330 #[serde(default)]
331 pub filter: Option<String>,
332 #[serde(flatten, default)]
333 pub kind_specific: BTreeMap<String, toml::Value>,
334}
335
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337pub struct TriggerSourceManifestEntry {
338 #[serde(default)]
339 pub id: Option<String>,
340 pub kind: TriggerKind,
341 pub provider: harn_vm::ProviderId,
342 #[serde(default, rename = "match")]
343 pub match_: Option<TriggerMatchExpr>,
344 #[serde(default)]
345 pub dedupe_key: Option<String>,
346 #[serde(default)]
347 pub retry: Option<TriggerRetrySpec>,
348 #[serde(default)]
349 pub priority: Option<TriggerPriorityField>,
350 #[serde(default)]
351 pub budget: Option<TriggerBudgetSpec>,
352 #[serde(default)]
353 pub concurrency: Option<TriggerConcurrencyManifestSpec>,
354 #[serde(default)]
355 pub throttle: Option<TriggerThrottleManifestSpec>,
356 #[serde(default)]
357 pub rate_limit: Option<TriggerRateLimitManifestSpec>,
358 #[serde(default)]
359 pub debounce: Option<TriggerDebounceManifestSpec>,
360 #[serde(default)]
361 pub singleton: Option<TriggerSingletonManifestSpec>,
362 #[serde(default)]
363 pub batch: Option<TriggerBatchManifestSpec>,
364 #[serde(default)]
365 pub window: Option<TriggerStreamWindowManifestSpec>,
366 #[serde(default)]
367 pub secrets: BTreeMap<String, String>,
368 #[serde(default)]
369 pub filter: Option<String>,
370 #[serde(flatten, default)]
371 pub kind_specific: BTreeMap<String, toml::Value>,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
375#[serde(rename_all = "kebab-case")]
376pub enum TriggerKind {
377 Webhook,
378 Cron,
379 Poll,
380 Stream,
381 Predicate,
382 A2aPush,
383}
384
385#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
386pub struct TriggerMatchExpr {
387 #[serde(default)]
388 pub events: Vec<String>,
389 #[serde(flatten, default)]
390 pub extra: BTreeMap<String, toml::Value>,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
394pub struct TriggerRetrySpec {
395 #[serde(default)]
396 pub max: u32,
397 #[serde(default)]
398 pub backoff: TriggerRetryBackoff,
399 #[serde(default = "default_trigger_retention_days")]
400 pub retention_days: u32,
401}
402
403#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
404#[serde(rename_all = "kebab-case")]
405pub enum TriggerRetryBackoff {
406 #[default]
407 Immediate,
408 Svix,
409}
410
411pub(crate) fn default_trigger_retention_days() -> u32 {
412 harn_vm::DEFAULT_INBOX_RETENTION_DAYS
413}
414
415impl Default for TriggerRetrySpec {
416 fn default() -> Self {
417 Self {
418 max: 0,
419 backoff: TriggerRetryBackoff::default(),
420 retention_days: default_trigger_retention_days(),
421 }
422 }
423}
424
425#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
426#[serde(rename_all = "lowercase")]
427pub enum TriggerDispatchPriority {
428 High,
429 #[default]
430 Normal,
431 Low,
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(untagged)]
436pub enum TriggerPriorityField {
437 Dispatch(TriggerDispatchPriority),
438 Flow(TriggerPriorityManifestSpec),
439}
440
441#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
442pub struct TriggerBudgetSpec {
443 #[serde(default)]
444 pub max_cost_usd: Option<f64>,
445 #[serde(default, alias = "tokens_max")]
446 pub max_tokens: Option<u64>,
447 #[serde(default)]
448 pub daily_cost_usd: Option<f64>,
449 #[serde(default)]
450 pub hourly_cost_usd: Option<f64>,
451 #[serde(default)]
452 pub max_autonomous_decisions_per_hour: Option<u64>,
453 #[serde(default)]
454 pub max_autonomous_decisions_per_day: Option<u64>,
455 #[serde(default)]
456 pub max_concurrent: Option<u32>,
457 #[serde(default)]
458 pub on_budget_exhausted: harn_vm::TriggerBudgetExhaustionStrategy,
459}
460
461#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
462pub struct TriggerWhenBudgetSpec {
463 #[serde(default)]
464 pub max_cost_usd: Option<f64>,
465 #[serde(default)]
466 pub tokens_max: Option<u64>,
467 #[serde(default)]
468 pub timeout: Option<String>,
469}
470
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472pub struct TriggerConcurrencyManifestSpec {
473 #[serde(default)]
474 pub key: Option<String>,
475 pub max: u32,
476}
477
478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
479pub struct TriggerThrottleManifestSpec {
480 #[serde(default)]
481 pub key: Option<String>,
482 pub period: String,
483 pub max: u32,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
487pub struct TriggerRateLimitManifestSpec {
488 #[serde(default)]
489 pub key: Option<String>,
490 pub period: String,
491 pub max: u32,
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
495pub struct TriggerDebounceManifestSpec {
496 pub key: String,
497 pub period: String,
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct TriggerSingletonManifestSpec {
502 #[serde(default)]
503 pub key: Option<String>,
504}
505
506#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
507pub struct TriggerBatchManifestSpec {
508 #[serde(default)]
509 pub key: Option<String>,
510 pub size: u32,
511 pub timeout: String,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
515pub struct TriggerPriorityManifestSpec {
516 pub key: String,
517 #[serde(default)]
518 pub order: Vec<String>,
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
522#[serde(rename_all = "kebab-case")]
523pub enum TriggerStreamWindowMode {
524 Tumbling,
525 Sliding,
526 Session,
527}
528
529#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
530pub struct TriggerStreamWindowManifestSpec {
531 pub mode: TriggerStreamWindowMode,
532 #[serde(default)]
533 pub key: Option<String>,
534 #[serde(default)]
535 pub size: Option<String>,
536 #[serde(default)]
537 pub every: Option<String>,
538 #[serde(default)]
539 pub gap: Option<String>,
540 #[serde(default)]
541 pub max_items: Option<u32>,
542}
543
544#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
545pub struct TriggerDlqAlertManifestSpec {
546 #[serde(default)]
547 pub destinations: Vec<TriggerDlqAlertDestination>,
548 #[serde(default)]
549 pub threshold: TriggerDlqAlertThreshold,
550}
551
552#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
553pub struct TriggerDlqAlertThreshold {
554 #[serde(default, alias = "entries-in-1h")]
555 pub entries_in_1h: Option<u32>,
556 #[serde(default, alias = "percent-of-dispatches")]
557 pub percent_of_dispatches: Option<f64>,
558}
559
560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
561#[serde(tag = "kind", rename_all = "snake_case")]
562pub enum TriggerDlqAlertDestination {
563 Slack {
564 channel: String,
565 #[serde(default)]
566 webhook_url_env: Option<String>,
567 },
568 Email {
569 address: String,
570 },
571 Webhook {
572 url: String,
573 #[serde(default)]
574 headers: BTreeMap<String, String>,
575 },
576}
577
578impl TriggerDlqAlertDestination {
579 pub fn label(&self) -> String {
580 match self {
581 Self::Slack { channel, .. } => format!("slack:{channel}"),
582 Self::Email { address } => format!("email:{address}"),
583 Self::Webhook { url, .. } => format!("webhook:{url}"),
584 }
585 }
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
589pub enum TriggerHandlerUri {
590 Local(TriggerFunctionRef),
591 A2a {
592 target: String,
593 allow_cleartext: bool,
594 },
595 Worker {
596 queue: String,
597 },
598 Persona {
599 name: String,
600 },
601 EvalPack {
602 target: String,
603 },
604}
605
606#[derive(Debug, Clone, PartialEq, Eq)]
607pub struct TriggerFunctionRef {
608 pub raw: String,
609 pub module_name: Option<String>,
610 pub function_name: String,
611}
612
613#[derive(Debug, Default, Clone, Deserialize)]
615#[allow(dead_code)] pub struct SkillsConfig {
617 #[serde(default)]
621 pub paths: Vec<String>,
622 #[serde(default)]
627 pub lookup_order: Vec<String>,
628 #[serde(default)]
630 pub disable: Vec<String>,
631 #[serde(default)]
634 pub signer_registry_url: Option<String>,
635 #[serde(default)]
639 pub defaults: SkillDefaults,
640}
641
642#[derive(Debug, Default, Clone, Deserialize)]
643#[allow(dead_code)] pub struct SkillDefaults {
645 #[serde(default)]
646 pub tool_search: Option<String>,
647 #[serde(default)]
648 pub always_loaded: Vec<String>,
649}
650
651#[derive(Debug, Default, Clone, Deserialize)]
653pub struct SkillTables {
654 #[serde(default, rename = "source")]
655 pub sources: Vec<SkillSourceEntry>,
656}
657
658#[derive(Debug, Clone, Deserialize)]
662#[serde(tag = "type", rename_all = "lowercase")]
663#[allow(dead_code)] pub enum SkillSourceEntry {
665 Fs {
666 path: String,
667 #[serde(default)]
668 namespace: Option<String>,
669 },
670 Git {
671 url: String,
672 #[serde(default)]
673 tag: Option<String>,
674 #[serde(default)]
675 namespace: Option<String>,
676 },
677 Registry {
678 #[serde(default)]
679 url: Option<String>,
680 #[serde(default)]
681 name: Option<String>,
682 },
683}
684
685#[derive(Debug, Default, Clone, Deserialize)]
686pub struct WorkspaceConfig {
687 #[serde(default)]
690 pub pipelines: Vec<String>,
691}
692
693#[derive(Debug, Default, Clone, Deserialize)]
694pub struct PackageRegistryConfig {
695 #[serde(default)]
697 pub url: Option<String>,
698}
699
700#[derive(Debug, Clone, Deserialize)]
701pub struct McpServerConfig {
702 pub name: String,
703 #[serde(default)]
704 pub transport: Option<String>,
705 #[serde(default)]
706 pub command: String,
707 #[serde(default)]
708 pub args: Vec<String>,
709 #[serde(default)]
710 pub env: HashMap<String, String>,
711 #[serde(default)]
712 pub url: String,
713 #[serde(default)]
714 pub auth_token: Option<String>,
715 #[serde(default)]
716 pub token_exchange: Option<harn_vm::mcp_oauth::McpTokenExchangeConfig>,
717 #[serde(default)]
718 pub auth: Option<McpAuthConfig>,
719 #[serde(default)]
720 pub client_id: Option<String>,
721 #[serde(default)]
722 pub client_secret: Option<String>,
723 #[serde(default)]
724 pub scopes: Option<String>,
725 #[serde(default)]
726 pub protocol_version: Option<String>,
727 #[serde(default)]
728 pub protocol_mode: Option<String>,
729 #[serde(default)]
730 pub proxy_server_name: Option<String>,
731 #[serde(default)]
735 pub lazy: bool,
736 #[serde(default)]
740 pub card: Option<String>,
741 #[serde(default, alias = "keep-alive-ms", alias = "keep_alive")]
745 pub keep_alive_ms: Option<u64>,
746}
747
748#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
749pub struct McpAuthConfig {
750 #[serde(default)]
751 pub mode: Option<harn_vm::mcp_auth::OAuthClientAuthMode>,
752 #[serde(default, alias = "client-id")]
753 pub client_id: Option<String>,
754 #[serde(
755 default,
756 alias = "client_secret_id",
757 alias = "client-secret-id",
758 alias = "client_secret_ref",
759 alias = "client-secret-ref"
760 )]
761 pub client_secret_id: Option<String>,
762 #[serde(
763 default,
764 alias = "secret_id",
765 alias = "secret-id",
766 alias = "token-secret-id"
767 )]
768 pub secret_id: Option<String>,
769 #[serde(default, alias = "scope")]
770 pub scopes: Option<String>,
771 #[serde(default, alias = "token_auth_method", alias = "token-auth-method")]
772 pub token_endpoint_auth_method: Option<String>,
773}
774
775#[derive(Debug, Clone, Deserialize)]
776#[allow(dead_code)] pub struct PackageInfo {
778 pub name: Option<String>,
779 pub version: Option<String>,
780 #[serde(default)]
781 pub evals: Vec<String>,
782 #[serde(default)]
783 pub description: Option<String>,
784 #[serde(default)]
785 pub license: Option<String>,
786 #[serde(default)]
787 pub repository: Option<String>,
788 #[serde(default, alias = "harn_version", alias = "harn_version_range")]
789 pub harn: Option<String>,
790 #[serde(default)]
791 pub docs_url: Option<String>,
792 #[serde(default)]
793 pub provenance: Option<String>,
794 #[serde(default)]
796 pub publisher: Option<String>,
797 #[serde(default)]
799 pub contact: Option<String>,
800 #[serde(default)]
803 pub created: Option<String>,
804 #[serde(default)]
805 pub permissions: Vec<String>,
806 #[serde(default, alias = "host-requirements")]
807 pub host_requirements: Vec<String>,
808 #[serde(default)]
809 pub tools: Vec<PackageToolExport>,
810 #[serde(default)]
811 pub skills: Vec<PackageSkillExport>,
812}
813
814#[derive(Debug, Clone, Deserialize, PartialEq)]
815pub struct PackageToolExport {
816 pub name: String,
817 pub module: String,
818 #[serde(default = "default_package_tool_symbol")]
819 pub symbol: String,
820 #[serde(default)]
821 pub description: Option<String>,
822 #[serde(default)]
823 pub permissions: Vec<String>,
824 #[serde(default, alias = "host-requirements")]
825 pub host_requirements: Vec<String>,
826 #[serde(default, alias = "input-schema")]
827 pub input_schema: Option<toml::Value>,
828 #[serde(default, alias = "output-schema")]
829 pub output_schema: Option<toml::Value>,
830 #[serde(default)]
831 pub annotations: BTreeMap<String, toml::Value>,
832}
833
834pub(crate) fn default_package_tool_symbol() -> String {
835 "tools".to_string()
836}
837
838#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
839pub struct PackageSkillExport {
840 pub name: String,
841 pub path: String,
842 #[serde(default)]
843 pub description: Option<String>,
844 #[serde(default)]
845 pub permissions: Vec<String>,
846 #[serde(default, alias = "host-requirements")]
847 pub host_requirements: Vec<String>,
848}
849
850#[derive(Debug, Clone, Deserialize)]
851#[serde(untagged)]
852pub enum Dependency {
853 Table(Box<DepTable>),
854 Path(String),
855}
856
857#[derive(Debug, Clone, Default, Deserialize)]
858pub struct DepTable {
859 pub git: Option<String>,
860 #[serde(default, alias = "archive-url", alias = "archive_url")]
861 pub archive: Option<String>,
862 pub tag: Option<String>,
863 pub rev: Option<String>,
864 pub branch: Option<String>,
865 pub version: Option<String>,
866 pub path: Option<String>,
867 pub package: Option<String>,
868 #[serde(default)]
869 pub checksum: Option<String>,
870 #[serde(default)]
875 pub registry: Option<String>,
876 #[serde(default, alias = "registry-name")]
879 pub registry_name: Option<String>,
880 #[serde(default, alias = "registry-version")]
882 pub registry_version: Option<String>,
883 #[serde(default, alias = "registry-commit")]
885 pub registry_commit: Option<String>,
886 #[serde(default, alias = "registry-provenance")]
888 pub registry_provenance: Option<String>,
889}
890
891impl Dependency {
892 pub(crate) fn git_url(&self) -> Option<&str> {
893 match self {
894 Dependency::Table(t) => t.git.as_deref(),
895 Dependency::Path(_) => None,
896 }
897 }
898
899 pub(crate) fn archive_url(&self) -> Option<&str> {
900 match self {
901 Dependency::Table(t) => t.archive.as_deref(),
902 Dependency::Path(_) => None,
903 }
904 }
905
906 pub(crate) fn rev(&self) -> Option<&str> {
907 match self {
908 Dependency::Table(t) => t.rev.as_deref(),
909 Dependency::Path(_) => None,
910 }
911 }
912
913 pub(crate) fn tag(&self) -> Option<&str> {
914 match self {
915 Dependency::Table(t) => t.tag.as_deref(),
916 Dependency::Path(_) => None,
917 }
918 }
919
920 pub(crate) fn branch(&self) -> Option<&str> {
921 match self {
922 Dependency::Table(t) => t.branch.as_deref(),
923 Dependency::Path(_) => None,
924 }
925 }
926
927 pub(crate) fn version(&self) -> Option<&str> {
928 match self {
929 Dependency::Table(t) => t.version.as_deref(),
930 Dependency::Path(_) => None,
931 }
932 }
933
934 pub(crate) fn requires_git(&self) -> bool {
935 self.git_url().is_some()
936 }
937
938 pub(crate) fn local_path(&self) -> Option<&str> {
939 match self {
940 Dependency::Table(t) => t.path.as_deref(),
941 Dependency::Path(p) => Some(p.as_str()),
942 }
943 }
944}
945
946pub(crate) fn validate_package_alias(alias: &str) -> Result<(), PackageError> {
947 if harn_modules::package_snapshot::is_valid_package_name(alias) {
948 Ok(())
949 } else {
950 Err(PackageError::Validation(format!(
951 "invalid dependency alias {alias:?}; use ASCII letters, numbers, '.', '_' or '-'"
952 )))
953 }
954}
955
956pub(crate) fn toml_string_literal(value: &str) -> Result<String, PackageError> {
957 use std::fmt::Write as _;
958
959 let mut encoded = String::with_capacity(value.len() + 2);
960 encoded.push('"');
961 for ch in value.chars() {
962 match ch {
963 '\u{08}' => encoded.push_str("\\b"),
964 '\t' => encoded.push_str("\\t"),
965 '\n' => encoded.push_str("\\n"),
966 '\u{0C}' => encoded.push_str("\\f"),
967 '\r' => encoded.push_str("\\r"),
968 '"' => encoded.push_str("\\\""),
969 '\\' => encoded.push_str("\\\\"),
970 ch if ch <= '\u{1F}' || ch == '\u{7F}' => {
971 write!(&mut encoded, "\\u{:04X}", ch as u32).map_err(|error| {
972 PackageError::Manifest(format!("failed to encode TOML string: {error}"))
973 })?;
974 }
975 ch => encoded.push(ch),
976 }
977 }
978 encoded.push('"');
979 Ok(encoded)
980}
981#[derive(Debug, Default, Clone)]
982pub struct RuntimeExtensions {
983 pub root_manifest: Option<Manifest>,
984 pub root_manifest_path: Option<PathBuf>,
985 pub root_manifest_dir: Option<PathBuf>,
986 pub(crate) runtime_personas: Vec<ResolvedRuntimePersona>,
987 pub llm: Option<harn_vm::llm_config::ProvidersConfig>,
988 pub capabilities: Option<harn_vm::llm::capabilities::CapabilitiesFile>,
989 pub hooks: Vec<ResolvedHookConfig>,
990 pub triggers: Vec<ResolvedTriggerConfig>,
991 pub handoff_routes: Vec<harn_vm::HandoffRouteConfig>,
992 pub provider_connectors: Vec<ResolvedProviderConnectorConfig>,
993}
994
995#[derive(Debug, Clone, Deserialize)]
996pub struct ProviderManifestEntry {
997 pub id: harn_vm::ProviderId,
998 pub connector: ProviderConnectorManifest,
999 #[serde(default)]
1000 pub oauth: Option<ProviderOAuthManifest>,
1001 #[serde(default)]
1002 pub setup: Option<ProviderSetupManifest>,
1003 #[serde(default)]
1004 pub capabilities: ConnectorCapabilities,
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, Default, PartialEq, Serialize, Deserialize)]
1036pub struct ProviderSetupManifest {
1037 #[serde(default, alias = "auth-type")]
1038 pub auth_type: Option<String>,
1039 #[serde(default)]
1040 pub flow: Option<String>,
1041 #[serde(default, alias = "required-scopes", alias = "scopes")]
1042 pub required_scopes: Vec<String>,
1043 #[serde(default, alias = "required-secrets")]
1044 pub required_secrets: Vec<String>,
1045 #[serde(default, alias = "setup-command")]
1046 pub setup_command: Vec<String>,
1047 #[serde(default, alias = "validation-command")]
1048 pub validation_command: Vec<String>,
1049 #[serde(default, alias = "health-checks")]
1050 pub health_checks: Vec<ConnectorHealthCheckManifest>,
1051 #[serde(default)]
1052 pub recovery: ConnectorRecoveryCopy,
1053 #[serde(flatten, default)]
1054 pub extra: BTreeMap<String, toml::Value>,
1055}
1056
1057#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1058pub struct ConnectorHealthCheckManifest {
1059 pub id: String,
1060 pub kind: String,
1061 #[serde(default)]
1062 pub command: Vec<String>,
1063 #[serde(default)]
1064 pub secret: Option<String>,
1065 #[serde(default)]
1066 pub url: Option<String>,
1067}
1068
1069#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1070pub struct ConnectorRecoveryCopy {
1071 #[serde(default, alias = "missing-install")]
1072 pub missing_install: Option<String>,
1073 #[serde(default, alias = "missing-auth")]
1074 pub missing_auth: Option<String>,
1075 #[serde(default, alias = "expired-credentials")]
1076 pub expired_credentials: Option<String>,
1077 #[serde(default, alias = "revoked-credentials")]
1078 pub revoked_credentials: Option<String>,
1079 #[serde(default, alias = "missing-scopes")]
1080 pub missing_scopes: Option<String>,
1081 #[serde(default, alias = "inaccessible-resource")]
1082 pub inaccessible_resource: Option<String>,
1083 #[serde(default, alias = "transient-provider-outage")]
1084 pub transient_provider_outage: Option<String>,
1085}
1086
1087#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
1088pub struct ConnectorCapabilities {
1089 pub webhook: bool,
1090 pub oauth: bool,
1091 pub rate_limit: bool,
1092 pub pagination: bool,
1093 pub graphql: bool,
1094 pub streaming: bool,
1095}
1096
1097impl ConnectorCapabilities {
1098 pub const FEATURES: [&'static str; 6] = [
1099 "webhook",
1100 "oauth",
1101 "rate_limit",
1102 "pagination",
1103 "graphql",
1104 "streaming",
1105 ];
1106
1107 fn enable(&mut self, feature: &str) -> Result<(), String> {
1108 match normalize_connector_capability(feature).as_str() {
1109 "webhook" => self.webhook = true,
1110 "oauth" => self.oauth = true,
1111 "rate_limit" => self.rate_limit = true,
1112 "pagination" => self.pagination = true,
1113 "graphql" => self.graphql = true,
1114 "streaming" => self.streaming = true,
1115 other => {
1116 return Err(format!(
1117 "unknown connector capability '{feature}' (normalized as '{other}')"
1118 ));
1119 }
1120 }
1121 Ok(())
1122 }
1123}
1124
1125#[derive(Debug, Default, Deserialize)]
1126struct ConnectorCapabilitiesTable {
1127 #[serde(default)]
1128 webhook: bool,
1129 #[serde(default)]
1130 oauth: bool,
1131 #[serde(default, alias = "rate-limit")]
1132 rate_limit: bool,
1133 #[serde(default)]
1134 pagination: bool,
1135 #[serde(default)]
1136 graphql: bool,
1137 #[serde(default)]
1138 streaming: bool,
1139}
1140
1141impl From<ConnectorCapabilitiesTable> for ConnectorCapabilities {
1142 fn from(value: ConnectorCapabilitiesTable) -> Self {
1143 Self {
1144 webhook: value.webhook,
1145 oauth: value.oauth,
1146 rate_limit: value.rate_limit,
1147 pagination: value.pagination,
1148 graphql: value.graphql,
1149 streaming: value.streaming,
1150 }
1151 }
1152}
1153
1154impl<'de> Deserialize<'de> for ConnectorCapabilities {
1155 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1156 where
1157 D: serde::Deserializer<'de>,
1158 {
1159 #[derive(Deserialize)]
1160 #[serde(untagged)]
1161 enum RawConnectorCapabilities {
1162 List(Vec<String>),
1163 Table(ConnectorCapabilitiesTable),
1164 }
1165
1166 match RawConnectorCapabilities::deserialize(deserializer)? {
1167 RawConnectorCapabilities::List(features) => {
1168 let mut capabilities = ConnectorCapabilities::default();
1169 for feature in features {
1170 capabilities
1171 .enable(&feature)
1172 .map_err(serde::de::Error::custom)?;
1173 }
1174 Ok(capabilities)
1175 }
1176 RawConnectorCapabilities::Table(table) => Ok(table.into()),
1177 }
1178 }
1179}
1180
1181pub fn normalize_connector_capability(feature: &str) -> String {
1182 feature.trim().to_lowercase().replace('-', "_")
1183}
1184
1185#[derive(Debug, Clone, Default, Deserialize)]
1186pub struct ConnectorContractConfig {
1187 #[serde(default)]
1188 pub version: Option<u32>,
1189 #[serde(default)]
1190 pub fixtures: Vec<ConnectorContractFixture>,
1191}
1192
1193#[derive(Debug, Clone, Deserialize)]
1194pub struct ConnectorContractFixture {
1195 pub provider: harn_vm::ProviderId,
1196 #[serde(default)]
1197 pub name: Option<String>,
1198 #[serde(default)]
1199 pub kind: Option<String>,
1200 #[serde(default)]
1201 pub headers: BTreeMap<String, String>,
1202 #[serde(default)]
1203 pub query: BTreeMap<String, String>,
1204 #[serde(default)]
1205 pub metadata: Option<toml::Value>,
1206 #[serde(default)]
1207 pub body: Option<String>,
1208 #[serde(default)]
1209 pub body_json: Option<toml::Value>,
1210 #[serde(default)]
1211 pub expect_type: Option<String>,
1212 #[serde(default)]
1213 pub expect_kind: Option<String>,
1214 #[serde(default)]
1215 pub expect_dedupe_key: Option<String>,
1216 #[serde(default)]
1217 pub expect_signature_state: Option<String>,
1218 #[serde(default)]
1219 pub expect_payload_contains: Option<toml::Value>,
1220 #[serde(default)]
1221 pub expect_response_status: Option<u16>,
1222 #[serde(default)]
1223 pub expect_response_body: Option<toml::Value>,
1224 #[serde(default)]
1225 pub expect_event_count: Option<usize>,
1226 #[serde(default)]
1227 pub expect_error_contains: Option<String>,
1228}
1229
1230#[derive(Debug, Clone, PartialEq, Eq)]
1231pub enum ResolvedProviderConnectorKind {
1232 Harn { module: String },
1233 RustBuiltin,
1234 Invalid(String),
1235}
1236
1237#[derive(Debug, Clone)]
1238pub struct ResolvedProviderConnectorConfig {
1239 pub id: harn_vm::ProviderId,
1240 pub manifest_dir: PathBuf,
1241 pub connector: ResolvedProviderConnectorKind,
1242 pub oauth: Option<ProviderOAuthManifest>,
1243 pub setup: Option<ProviderSetupManifest>,
1244}
1245
1246#[derive(Debug, Clone)]
1247pub struct ResolvedHookConfig {
1248 pub event: harn_vm::orchestration::HookEvent,
1249 pub pattern: String,
1250 pub handler: String,
1251 pub manifest_dir: PathBuf,
1252 pub package_name: Option<String>,
1253 pub exports: HashMap<String, String>,
1254}
1255
1256#[derive(Debug, Clone)]
1257pub struct ResolvedTriggerConfig {
1258 pub id: String,
1259 pub kind: TriggerKind,
1260 pub provider: harn_vm::ProviderId,
1261 pub autonomy_tier: harn_vm::AutonomyTier,
1262 pub match_: TriggerMatchExpr,
1263 pub when: Option<String>,
1264 pub when_budget: Option<TriggerWhenBudgetSpec>,
1265 pub handler: String,
1266 pub dedupe_key: Option<String>,
1267 pub retry: TriggerRetrySpec,
1268 pub dispatch_priority: TriggerDispatchPriority,
1269 pub budget: TriggerBudgetSpec,
1270 pub concurrency: Option<TriggerConcurrencyManifestSpec>,
1271 pub throttle: Option<TriggerThrottleManifestSpec>,
1272 pub rate_limit: Option<TriggerRateLimitManifestSpec>,
1273 pub debounce: Option<TriggerDebounceManifestSpec>,
1274 pub singleton: Option<TriggerSingletonManifestSpec>,
1275 pub batch: Option<TriggerBatchManifestSpec>,
1276 pub window: Option<TriggerStreamWindowManifestSpec>,
1277 pub priority_flow: Option<TriggerPriorityManifestSpec>,
1278 pub secrets: BTreeMap<String, String>,
1279 pub filter: Option<String>,
1280 pub kind_specific: BTreeMap<String, toml::Value>,
1281 pub manifest_dir: PathBuf,
1282 pub manifest_path: PathBuf,
1283 pub package_name: Option<String>,
1284 pub exports: HashMap<String, String>,
1285 pub execution_guard: Option<Arc<harn_modules::package_execution::PackageExecutionGuard>>,
1286 pub table_index: usize,
1287 pub shape_error: Option<String>,
1288}
1289
1290#[derive(Debug, Clone)]
1291#[allow(dead_code)] pub struct CollectedManifestTrigger {
1293 pub config: ResolvedTriggerConfig,
1294 pub handler: CollectedTriggerHandler,
1295 pub when: Option<CollectedTriggerPredicate>,
1296 pub flow_control: harn_vm::TriggerFlowControlConfig,
1297}
1298
1299#[derive(Debug, Clone)]
1300#[allow(dead_code)] pub enum CollectedTriggerHandler {
1302 Local {
1303 reference: TriggerFunctionRef,
1304 callable: harn_vm::VmCallable,
1305 },
1306 A2a {
1307 target: String,
1308 allow_cleartext: bool,
1309 },
1310 Worker {
1311 queue: String,
1312 },
1313 Persona {
1314 binding: harn_vm::PersonaRuntimeBinding,
1315 callable: harn_vm::VmCallable,
1316 },
1317 EvalPack {
1318 target: String,
1319 manifest: Box<harn_vm::orchestration::EvalPackManifest>,
1320 ledger_options: Option<serde_json::Value>,
1321 },
1322}
1323#[derive(Debug, Clone)]
1324#[allow(dead_code)] pub struct CollectedTriggerPredicate {
1326 pub reference: TriggerFunctionRef,
1327 pub callable: harn_vm::VmCallable,
1328}
1329
1330pub(crate) type ManifestModuleCacheKey = (PathBuf, Option<String>, Option<String>);
1331pub(crate) type ManifestModuleExports = BTreeMap<String, Arc<harn_vm::VmClosure>>;
1332
1333static MANIFEST_PROVIDER_SCHEMA_LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
1334
1335pub(crate) async fn lock_manifest_provider_schemas() -> tokio::sync::MutexGuard<'static, ()> {
1336 MANIFEST_PROVIDER_SCHEMA_LOCK
1337 .get_or_init(|| tokio::sync::Mutex::new(()))
1338 .lock()
1339 .await
1340}
1341
1342fn llm_manifest_diagnostics(content: &str) -> Vec<harn_vm::llm_config::ProviderConfigDiagnostic> {
1343 let Ok(value) = toml::from_str::<toml::Value>(content) else {
1344 return Vec::new();
1345 };
1346 let Some(llm) = value.get("llm") else {
1347 return Vec::new();
1348 };
1349 let Ok(llm_src) = toml::to_string(llm) else {
1350 return Vec::new();
1351 };
1352 let Ok(parsed) = harn_vm::llm_config::parse_config_toml_with_diagnostics(&llm_src) else {
1353 return Vec::new();
1354 };
1355 parsed
1356 .diagnostics
1357 .into_iter()
1358 .map(|mut diagnostic| {
1359 if !diagnostic.path.is_empty() {
1360 diagnostic.path = format!("llm.{}", diagnostic.path);
1361 }
1362 diagnostic
1363 })
1364 .collect()
1365}
1366
1367pub(crate) fn read_manifest_from_path(path: &Path) -> Result<Manifest, PackageError> {
1368 let content = fs::read_to_string(path).map_err(|error| {
1369 if error.kind() == std::io::ErrorKind::NotFound {
1370 PackageError::Manifest(format!(
1371 "No {} found in {}.",
1372 MANIFEST,
1373 path.parent().unwrap_or_else(|| Path::new(".")).display()
1374 ))
1375 } else {
1376 PackageError::Manifest(format!("failed to read {}: {error}", path.display()))
1377 }
1378 })?;
1379 let manifest = toml::from_str::<Manifest>(&content).map_err(|error| {
1380 PackageError::Manifest(format!("failed to parse {}: {error}", path.display()))
1381 })?;
1382 for diagnostic in llm_manifest_diagnostics(&content) {
1383 eprintln!("[llm_config] warning in {}: {diagnostic}", path.display());
1384 }
1385 Ok(manifest)
1386}
1387
1388pub(crate) fn absolutize_check_config_paths(
1389 mut config: CheckConfig,
1390 manifest_dir: &Path,
1391) -> CheckConfig {
1392 if let Some(path) = config.host_capabilities_path.clone() {
1393 let candidate = PathBuf::from(&path);
1394 if !candidate.is_absolute() {
1395 config.host_capabilities_path =
1396 Some(manifest_dir.join(candidate).display().to_string());
1397 }
1398 }
1399 if let Some(path) = config.bundle_root.clone() {
1400 let candidate = PathBuf::from(&path);
1401 if !candidate.is_absolute() {
1402 config.bundle_root = Some(manifest_dir.join(candidate).display().to_string());
1403 }
1404 }
1405 config
1406}
1407
1408pub fn load_check_config(harn_file: Option<&std::path::Path>) -> CheckConfig {
1412 let anchor = harn_file
1413 .map(Path::to_path_buf)
1414 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1415 if let Some((manifest, dir)) = nearest_manifest_or_warn(&anchor) {
1416 return absolutize_check_config_paths(manifest.check, &dir);
1417 }
1418 CheckConfig::default()
1419}
1420
1421pub fn load_workspace_config(anchor: Option<&Path>) -> Option<(WorkspaceConfig, PathBuf)> {
1425 let anchor = anchor
1426 .map(Path::to_path_buf)
1427 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1428 let (manifest, dir) = nearest_manifest_or_warn(&anchor)?;
1429 Some((manifest.workspace, dir))
1430}
1431
1432pub fn load_package_eval_pack_paths(anchor: Option<&Path>) -> Result<Vec<PathBuf>, PackageError> {
1433 let anchor = anchor
1434 .map(Path::to_path_buf)
1435 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1436 let Some((manifest, dir)) = load_nearest_manifest(&anchor).into_result()? else {
1437 return Err(PackageError::Manifest(
1438 "no harn.toml found for package eval discovery".to_string(),
1439 ));
1440 };
1441
1442 let ctx = ManifestContext { manifest, dir };
1443 let mut paths = eval_pack_paths_from_manifest(&ctx.manifest, &ctx.dir)?;
1444 paths.extend(installed_package_eval_pack_paths(&ctx)?);
1445 paths.sort();
1446 paths.dedup();
1447 if paths.is_empty() {
1448 return Err(PackageError::Manifest(
1449 "package declares no eval packs; add [package].evals, harn.eval.toml, or install a dependency that ships eval packs".to_string(),
1450 ));
1451 }
1452 for path in &paths {
1453 if !path.is_file() {
1454 return Err(PackageError::Manifest(format!(
1455 "eval pack does not exist: {}",
1456 path.display()
1457 )));
1458 }
1459 }
1460 Ok(paths)
1461}
1462
1463fn eval_pack_paths_from_manifest(
1464 manifest: &Manifest,
1465 manifest_dir: &Path,
1466) -> Result<Vec<PathBuf>, PackageError> {
1467 let declared = manifest
1468 .package
1469 .as_ref()
1470 .map(|package| package.evals.clone())
1471 .unwrap_or_default();
1472 let paths = if declared.is_empty() {
1473 let default_pack = manifest_dir.join("harn.eval.toml");
1474 if default_pack.is_file() {
1475 vec![default_pack]
1476 } else {
1477 Vec::new()
1478 }
1479 } else {
1480 declared
1481 .iter()
1482 .map(|entry| {
1483 let path = PathBuf::from(entry);
1484 if path.is_absolute() {
1485 path
1486 } else {
1487 manifest_dir.join(path)
1488 }
1489 })
1490 .collect()
1491 };
1492 for path in &paths {
1493 if !path.is_file() {
1494 return Err(PackageError::Manifest(format!(
1495 "eval pack does not exist: {}",
1496 path.display()
1497 )));
1498 }
1499 }
1500 Ok(paths)
1501}
1502
1503fn installed_package_eval_pack_paths(ctx: &ManifestContext) -> Result<Vec<PathBuf>, PackageError> {
1504 let Some(snapshot) = dependency_package_snapshot(&ctx.manifest, &ctx.dir)? else {
1505 return Ok(Vec::new());
1506 };
1507 let lock = LockFile::load(snapshot.lock_path())?.ok_or_else(|| {
1508 PackageError::Lockfile(format!(
1509 "published package generation is missing {}",
1510 snapshot.lock_path().display()
1511 ))
1512 })?;
1513 let mut paths = Vec::new();
1514 let packages_dir = snapshot.packages_root();
1515 for entry in &lock.packages {
1516 validate_package_alias(&entry.name)?;
1517 let package_dir = packages_dir.join(&entry.name);
1518 if package_dir.is_dir() {
1519 if let Some(manifest) = read_package_manifest_from_dir(&package_dir)? {
1520 paths.extend(eval_pack_paths_from_manifest(&manifest, &package_dir)?);
1521 }
1522 continue;
1523 }
1524
1525 let package_file = packages_dir.join(format!("{}.harn", entry.name));
1526 if package_file.is_file() {
1527 continue;
1528 }
1529
1530 return Err(PackageError::Manifest(format!(
1531 "installed package {} is missing under {}; run `harn install`",
1532 entry.name,
1533 packages_dir.display()
1534 )));
1535 }
1536 Ok(paths)
1537}
1538
1539#[derive(Debug, Clone)]
1540pub(crate) struct ManifestContext {
1541 pub(crate) manifest: Manifest,
1542 pub(crate) dir: PathBuf,
1543}
1544
1545impl ManifestContext {
1546 pub(crate) fn manifest_path(&self) -> PathBuf {
1547 self.dir.join(MANIFEST)
1548 }
1549
1550 pub(crate) fn lock_path(&self) -> PathBuf {
1551 self.dir.join(LOCK_FILE)
1552 }
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557 use super::*;
1558 use crate::package::test_support::{current_packages_dir, TestWorkspace};
1559
1560 #[test]
1561 fn rules_table_parses_camel_and_kebab_dir_keys() {
1562 let camel: Manifest =
1565 toml::from_str("[rules]\nruleDirs = [\"rules\", \"vendor/rules\"]\n").unwrap();
1566 assert_eq!(camel.rules.rule_dirs, vec!["rules", "vendor/rules"]);
1567
1568 let kebab: Manifest = toml::from_str("[rules]\nrule-dirs = [\"r\"]\n").unwrap();
1569 assert_eq!(kebab.rules.rule_dirs, vec!["r"]);
1570
1571 let native: Manifest =
1572 toml::from_str("[rules]\nnativeRuleDirs = [\"native-rules\"]\n").unwrap();
1573 assert_eq!(native.rules.native_rule_dirs, vec!["native-rules"]);
1574
1575 let native_kebab: Manifest =
1576 toml::from_str("[rules]\nnative-rule-dirs = [\"nr\"]\n").unwrap();
1577 assert_eq!(native_kebab.rules.native_rule_dirs, vec!["nr"]);
1578
1579 let none: Manifest = toml::from_str("[package]\nname = \"x\"\n").unwrap();
1581 assert!(none.rules.rule_dirs.is_empty());
1582 assert!(none.rules.native_rule_dirs.is_empty());
1583 }
1584
1585 #[test]
1586 fn llm_manifest_diagnostics_report_unknown_model_fields() {
1587 let diagnostics = llm_manifest_diagnostics(
1588 r#"
1589[llm.models."demo/model"]
1590name = "Demo"
1591provider = "demo"
1592context_window = 4096
1593fast_mode = true
1594"#,
1595 );
1596 let texts: Vec<String> = diagnostics
1597 .into_iter()
1598 .map(|diagnostic| diagnostic.to_string())
1599 .collect();
1600 assert!(
1601 texts.iter().any(
1602 |diagnostic| diagnostic.contains("llm.models.demo/model.fast_mode")
1603 && diagnostic.contains("serving_tiers")
1604 ),
1605 "expected manifest [llm] unknown-field diagnostic, got {texts:?}"
1606 );
1607 }
1608
1609 #[test]
1610 fn package_eval_pack_paths_use_package_manifest_entries() {
1611 let tmp = tempfile::tempdir().unwrap();
1612 let root = tmp.path();
1613 fs::create_dir_all(root.join(".git")).unwrap();
1614 fs::create_dir_all(root.join("evals")).unwrap();
1615 fs::write(
1616 root.join(MANIFEST),
1617 r#"
1618 [package]
1619 name = "demo"
1620 version = "0.1.0"
1621 evals = ["evals/webhook.toml"]
1622 "#,
1623 )
1624 .unwrap();
1625 fs::write(
1626 root.join("evals/webhook.toml"),
1627 "version = 1\n[[cases]]\nrun = \"run.json\"\n",
1628 )
1629 .unwrap();
1630
1631 let paths = load_package_eval_pack_paths(Some(&root.join("src/main.harn"))).unwrap();
1632
1633 assert_eq!(paths, vec![root.join("evals/webhook.toml")]);
1634 assert!(
1635 !root.join(".harn").exists(),
1636 "loading project eval packs without dependencies must remain read-only"
1637 );
1638 }
1639
1640 #[test]
1641 fn package_eval_pack_paths_include_installed_package_evals() {
1642 let dependency_tmp = tempfile::tempdir().unwrap();
1643 let dependency = dependency_tmp.path().join("coding-pack");
1644 fs::create_dir_all(dependency.join("evals")).unwrap();
1645 fs::write(
1646 dependency.join(MANIFEST),
1647 r#"
1648[package]
1649name = "coding-pack"
1650version = "0.1.0"
1651evals = ["evals/coding.toml"]
1652"#,
1653 )
1654 .unwrap();
1655 fs::write(
1656 dependency.join("evals/run.json"),
1657 serde_json::to_string_pretty(&serde_json::json!({
1658 "_type": "workflow_run",
1659 "id": "run_1",
1660 "workflow_id": "workflow_1",
1661 "status": "completed",
1662 "usage": {
1663 "total_duration_ms": 12,
1664 "total_cost": 0.01,
1665 "input_tokens": 3,
1666 "output_tokens": 4,
1667 "call_count": 1,
1668 "models": ["mock"]
1669 },
1670 "replay_fixture": {
1671 "_type": "replay_fixture",
1672 "expected_status": "completed"
1673 }
1674 }))
1675 .unwrap(),
1676 )
1677 .unwrap();
1678 fs::write(
1679 dependency.join("evals/coding.toml"),
1680 r#"
1681version = 1
1682id = "coding-pack"
1683trials = 2
1684
1685[package]
1686name = "coding-pack"
1687version = "0.1.0"
1688source = "path:test"
1689templates = ["templates/rubric.harn.prompt"]
1690
1691[metadata]
1692model = "mock-model"
1693commit = "commit-a"
1694
1695[[cases]]
1696id = "case-a"
1697run = "run.json"
1698rubrics = ["status"]
1699
1700[[rubrics]]
1701id = "status"
1702kind = "deterministic"
1703
1704[[rubrics.assertions]]
1705kind = "run-status"
1706expected = "completed"
1707"#,
1708 )
1709 .unwrap();
1710
1711 let helper = dependency_tmp.path().join("helper-lib");
1712 fs::create_dir_all(&helper).unwrap();
1713 fs::write(
1714 helper.join(MANIFEST),
1715 r#"
1716[package]
1717name = "helper-lib"
1718version = "0.1.0"
1719"#,
1720 )
1721 .unwrap();
1722
1723 let project_tmp = tempfile::tempdir().unwrap();
1724 let root = project_tmp.path();
1725 let workspace = TestWorkspace::new(root);
1726 fs::create_dir_all(root.join(".git")).unwrap();
1727 fs::write(
1728 root.join(MANIFEST),
1729 format!(
1730 r#"
1731[package]
1732name = "workspace"
1733version = "0.1.0"
1734
1735[dependencies]
1736coding-pack = {{ path = {} }}
1737helper-lib = {{ path = {} }}
1738"#,
1739 crate::format::toml_basic_string_literal(&dependency.display().to_string()),
1740 crate::format::toml_basic_string_literal(&helper.display().to_string())
1741 ),
1742 )
1743 .unwrap();
1744
1745 install_packages_in(workspace.env(), false, None, false).unwrap();
1746
1747 let paths = load_package_eval_pack_paths(Some(&root.join("src/main.harn"))).unwrap();
1748 assert_eq!(
1749 paths,
1750 vec![current_packages_dir(root)
1751 .join("coding-pack")
1752 .join("evals/coding.toml")]
1753 );
1754
1755 harn_vm::event_log::reset_active_event_log();
1756 let manifest = harn_vm::orchestration::load_eval_pack_manifest(&paths[0]).unwrap();
1757 let package = manifest.package.as_ref().expect("package descriptor");
1758 assert_eq!(package.name.as_deref(), Some("coding-pack"));
1759 assert_eq!(package.templates, vec!["templates/rubric.harn.prompt"]);
1760
1761 let report = harn_vm::orchestration::evaluate_eval_pack_manifest_resumable(
1762 &manifest,
1763 Some(serde_json::json!({
1764 "namespace": "installed-pack-evals",
1765 "suite": "coding-pack",
1766 "model": "mock-model",
1767 "commit": "commit-a",
1768 "branch": "main"
1769 })),
1770 )
1771 .unwrap();
1772 assert!(report.pass);
1773 assert_eq!(report.trial_count, 2);
1774 assert_eq!(report.run_state.ledger_rows_inserted, 2);
1775 assert_eq!(report.stats_rows.len(), 1);
1776 assert_eq!(report.stats_rows[0].trials, 2);
1777 assert!(!report.stats_rows[0].case_fingerprint.is_empty());
1778 assert_eq!(
1779 report.harness_config_fingerprint,
1780 report.stats_rows[0].harness_config_fingerprint
1781 );
1782
1783 let ledger = harn_vm::orchestration::eval_ledger_read_report(Some(serde_json::json!({
1784 "namespace": "installed-pack-evals",
1785 "suite": "coding-pack",
1786 "model": "mock-model",
1787 "commit": "commit-a"
1788 })))
1789 .unwrap();
1790 assert_eq!(ledger.rows.len(), 2);
1791 harn_vm::event_log::reset_active_event_log();
1792 }
1793 #[test]
1794 fn preflight_severity_parsing_accepts_synonyms() {
1795 assert_eq!(
1796 PreflightSeverity::from_opt(Some("warning")),
1797 PreflightSeverity::Warning
1798 );
1799 assert_eq!(
1800 PreflightSeverity::from_opt(Some("WARN")),
1801 PreflightSeverity::Warning
1802 );
1803 assert_eq!(
1804 PreflightSeverity::from_opt(Some("off")),
1805 PreflightSeverity::Off
1806 );
1807 assert_eq!(
1808 PreflightSeverity::from_opt(Some("allow")),
1809 PreflightSeverity::Off
1810 );
1811 assert_eq!(
1812 PreflightSeverity::from_opt(Some("error")),
1813 PreflightSeverity::Error
1814 );
1815 assert_eq!(PreflightSeverity::from_opt(None), PreflightSeverity::Error);
1816 assert_eq!(
1818 PreflightSeverity::from_opt(Some("bogus")),
1819 PreflightSeverity::Error
1820 );
1821 }
1822
1823 #[test]
1824 fn load_check_config_walks_up_from_nested_file() {
1825 let tmp = tempfile::tempdir().unwrap();
1826 let root = tmp.path();
1827 std::fs::create_dir_all(root.join(".git")).unwrap();
1829 fs::write(
1830 root.join(MANIFEST),
1831 r#"
1832 [check]
1833 preflight_severity = "warning"
1834 preflight_allow = ["custom.scan", "runtime.*"]
1835 host_capabilities_path = "./schemas/host-caps.json"
1836
1837 [workspace]
1838 pipelines = ["pipelines", "scripts"]
1839 "#,
1840 )
1841 .unwrap();
1842 let nested = root.join("src").join("deep");
1843 std::fs::create_dir_all(&nested).unwrap();
1844 let harn_file = nested.join("pipeline.harn");
1845 fs::write(&harn_file, "pipeline main() {}\n").unwrap();
1846
1847 let cfg = load_check_config(Some(&harn_file));
1848 assert_eq!(cfg.preflight_severity.as_deref(), Some("warning"));
1849 assert_eq!(cfg.preflight_allow, vec!["custom.scan", "runtime.*"]);
1850 let caps_path = cfg.host_capabilities_path.expect("host caps path");
1851 assert!(
1852 caps_path.ends_with("schemas/host-caps.json")
1853 || caps_path.ends_with("schemas\\host-caps.json"),
1854 "unexpected absolutized path: {caps_path}"
1855 );
1856
1857 let (workspace, manifest_dir) =
1858 load_workspace_config(Some(&harn_file)).expect("workspace manifest");
1859 assert_eq!(workspace.pipelines, vec!["pipelines", "scripts"]);
1860 assert_eq!(manifest_dir, root);
1862 }
1863
1864 #[test]
1865 fn toml_string_literal_escapes_all_basic_control_characters() {
1866 let literal = toml_string_literal("a\u{08}\t\n\u{0C}\r\"\\\u{07}z").unwrap();
1867 let parsed: toml::Value = toml::from_str(&format!("value = {literal}\n")).unwrap();
1868 assert_eq!(
1869 parsed.get("value").and_then(toml::Value::as_str),
1870 Some("a\u{08}\t\n\u{0C}\r\"\\\u{07}z")
1871 );
1872 }
1873
1874 #[test]
1875 fn orchestrator_drain_config_parses_defaults_and_overrides() {
1876 let default_manifest: Manifest = toml::from_str(
1877 r#"
1878 [package]
1879 name = "fixture"
1880 "#,
1881 )
1882 .unwrap();
1883 assert_eq!(default_manifest.orchestrator.drain.max_items, 1024);
1884 assert_eq!(default_manifest.orchestrator.drain.deadline_seconds, 30);
1885 assert_eq!(default_manifest.orchestrator.pumps.max_outstanding, 64);
1886
1887 let configured: Manifest = toml::from_str(
1888 r#"
1889 [package]
1890 name = "fixture"
1891
1892 [orchestrator]
1893 drain.max_items = 77
1894 drain.deadline_seconds = 12
1895 pumps.max_outstanding = 3
1896 "#,
1897 )
1898 .unwrap();
1899 assert_eq!(configured.orchestrator.drain.max_items, 77);
1900 assert_eq!(configured.orchestrator.drain.deadline_seconds, 12);
1901 assert_eq!(configured.orchestrator.pumps.max_outstanding, 3);
1902 }
1903
1904 #[test]
1905 fn load_check_config_stops_at_git_boundary() {
1906 let tmp = tempfile::tempdir().unwrap();
1907 fs::write(
1909 tmp.path().join(MANIFEST),
1910 "[check]\npreflight_severity = \"off\"\n",
1911 )
1912 .unwrap();
1913 let project = tmp.path().join("project");
1914 std::fs::create_dir_all(project.join(".git")).unwrap();
1915 let inner = project.join("src");
1916 std::fs::create_dir_all(&inner).unwrap();
1917 let harn_file = inner.join("main.harn");
1918 fs::write(&harn_file, "pipeline main() {}\n").unwrap();
1919 let cfg = load_check_config(Some(&harn_file));
1920 assert!(
1921 cfg.preflight_severity.is_none(),
1922 "must not inherit harn.toml from outside the .git boundary"
1923 );
1924 }
1925}