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