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