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