1use super::errors::PackageError;
2use super::*;
3pub use harn_modules::personas::{
4 PersonaAutonomyTier, PersonaManifestEntry, PersonaStageDecl, PersonaStageExit,
5 PersonaValidationError, ResolvedPersonaManifest,
6};
7
8#[derive(Debug, Clone, Deserialize)]
9pub struct Manifest {
10 pub package: Option<PackageInfo>,
11 #[serde(default)]
12 pub dependencies: HashMap<String, Dependency>,
13 #[serde(default)]
14 pub mcp: Vec<McpServerConfig>,
15 #[serde(default)]
16 pub check: CheckConfig,
17 #[serde(default)]
18 pub workspace: WorkspaceConfig,
19 #[serde(default)]
23 pub registry: PackageRegistryConfig,
24 #[serde(default)]
27 pub skills: SkillsConfig,
28 #[serde(default)]
31 pub skill: SkillTables,
32 #[serde(default)]
40 pub capabilities: Option<harn_vm::llm::capabilities::CapabilitiesFile>,
41 #[serde(default)]
45 pub exports: HashMap<String, String>,
46 #[serde(default)]
51 pub llm: harn_vm::llm_config::ProvidersConfig,
52 #[serde(default)]
57 pub hooks: Vec<HookConfig>,
58 #[serde(default)]
63 pub triggers: Vec<TriggerManifestEntry>,
64 #[serde(default)]
68 pub handoff_routes: Vec<harn_vm::HandoffRouteConfig>,
69 #[serde(default)]
73 pub providers: Vec<ProviderManifestEntry>,
74 #[serde(default)]
78 pub personas: Vec<PersonaManifestEntry>,
79 #[serde(default, alias = "connector-contract")]
82 pub connector_contract: ConnectorContractConfig,
83 #[serde(default)]
86 pub orchestrator: OrchestratorConfig,
87}
88
89#[derive(Debug, Clone, Default, Deserialize)]
90pub struct OrchestratorConfig {
91 #[serde(default, alias = "allowed-origins")]
92 pub allowed_origins: Vec<String>,
93 #[serde(default, alias = "max-body-bytes")]
94 pub max_body_bytes: Option<usize>,
95 #[serde(default)]
96 pub budget: OrchestratorBudgetSpec,
97 #[serde(default)]
98 pub drain: OrchestratorDrainConfig,
99 #[serde(default)]
100 pub pumps: OrchestratorPumpConfig,
101}
102
103#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
104pub struct OrchestratorBudgetSpec {
105 #[serde(default)]
106 pub daily_cost_usd: Option<f64>,
107 #[serde(default)]
108 pub hourly_cost_usd: Option<f64>,
109}
110
111#[derive(Debug, Clone, Deserialize)]
112pub struct OrchestratorDrainConfig {
113 #[serde(default = "default_orchestrator_drain_max_items", alias = "max-items")]
114 pub max_items: usize,
115 #[serde(
116 default = "default_orchestrator_drain_deadline_seconds",
117 alias = "deadline-seconds"
118 )]
119 pub deadline_seconds: u64,
120}
121
122impl Default for OrchestratorDrainConfig {
123 fn default() -> Self {
124 Self {
125 max_items: default_orchestrator_drain_max_items(),
126 deadline_seconds: default_orchestrator_drain_deadline_seconds(),
127 }
128 }
129}
130
131pub(crate) fn default_orchestrator_drain_max_items() -> usize {
132 1024
133}
134
135pub(crate) fn default_orchestrator_drain_deadline_seconds() -> u64 {
136 30
137}
138
139#[derive(Debug, Clone, Deserialize)]
140pub struct OrchestratorPumpConfig {
141 #[serde(
142 default = "default_orchestrator_pump_max_outstanding",
143 alias = "max-outstanding"
144 )]
145 pub max_outstanding: usize,
146}
147
148impl Default for OrchestratorPumpConfig {
149 fn default() -> Self {
150 Self {
151 max_outstanding: default_orchestrator_pump_max_outstanding(),
152 }
153 }
154}
155
156pub(crate) fn default_orchestrator_pump_max_outstanding() -> usize {
157 64
158}
159
160#[derive(Debug, Clone, Deserialize)]
161pub struct HookConfig {
162 pub event: harn_vm::orchestration::HookEvent,
163 #[serde(default = "default_hook_pattern")]
164 pub pattern: String,
165 pub handler: String,
166}
167
168pub(crate) fn default_hook_pattern() -> String {
169 "*".to_string()
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct TriggerManifestEntry {
174 pub id: String,
175 #[serde(default)]
176 pub kind: Option<TriggerKind>,
177 #[serde(default)]
178 pub provider: Option<harn_vm::ProviderId>,
179 #[serde(default, alias = "tier")]
180 pub autonomy_tier: harn_vm::AutonomyTier,
181 #[serde(default, rename = "match")]
182 pub match_: Option<TriggerMatchExpr>,
183 #[serde(default)]
184 pub sources: Vec<TriggerSourceManifestEntry>,
185 #[serde(default)]
186 pub when: Option<String>,
187 #[serde(default)]
188 pub when_budget: Option<TriggerWhenBudgetSpec>,
189 pub handler: String,
190 #[serde(default)]
191 pub dedupe_key: Option<String>,
192 #[serde(default)]
193 pub retry: TriggerRetrySpec,
194 #[serde(default)]
195 pub priority: Option<TriggerPriorityField>,
196 #[serde(default)]
197 pub budget: TriggerBudgetSpec,
198 #[serde(default)]
199 pub concurrency: Option<TriggerConcurrencyManifestSpec>,
200 #[serde(default)]
201 pub throttle: Option<TriggerThrottleManifestSpec>,
202 #[serde(default)]
203 pub rate_limit: Option<TriggerRateLimitManifestSpec>,
204 #[serde(default)]
205 pub debounce: Option<TriggerDebounceManifestSpec>,
206 #[serde(default)]
207 pub singleton: Option<TriggerSingletonManifestSpec>,
208 #[serde(default)]
209 pub batch: Option<TriggerBatchManifestSpec>,
210 #[serde(default)]
211 pub window: Option<TriggerStreamWindowManifestSpec>,
212 #[serde(default, alias = "dlq-alerts")]
213 pub dlq_alerts: Vec<TriggerDlqAlertManifestSpec>,
214 #[serde(default)]
215 pub secrets: BTreeMap<String, String>,
216 #[serde(default)]
217 pub filter: Option<String>,
218 #[serde(flatten, default)]
219 pub kind_specific: BTreeMap<String, toml::Value>,
220}
221
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223pub struct TriggerSourceManifestEntry {
224 #[serde(default)]
225 pub id: Option<String>,
226 pub kind: TriggerKind,
227 pub provider: harn_vm::ProviderId,
228 #[serde(default, rename = "match")]
229 pub match_: Option<TriggerMatchExpr>,
230 #[serde(default)]
231 pub dedupe_key: Option<String>,
232 #[serde(default)]
233 pub retry: Option<TriggerRetrySpec>,
234 #[serde(default)]
235 pub priority: Option<TriggerPriorityField>,
236 #[serde(default)]
237 pub budget: Option<TriggerBudgetSpec>,
238 #[serde(default)]
239 pub concurrency: Option<TriggerConcurrencyManifestSpec>,
240 #[serde(default)]
241 pub throttle: Option<TriggerThrottleManifestSpec>,
242 #[serde(default)]
243 pub rate_limit: Option<TriggerRateLimitManifestSpec>,
244 #[serde(default)]
245 pub debounce: Option<TriggerDebounceManifestSpec>,
246 #[serde(default)]
247 pub singleton: Option<TriggerSingletonManifestSpec>,
248 #[serde(default)]
249 pub batch: Option<TriggerBatchManifestSpec>,
250 #[serde(default)]
251 pub window: Option<TriggerStreamWindowManifestSpec>,
252 #[serde(default)]
253 pub secrets: BTreeMap<String, String>,
254 #[serde(default)]
255 pub filter: Option<String>,
256 #[serde(flatten, default)]
257 pub kind_specific: BTreeMap<String, toml::Value>,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261#[serde(rename_all = "kebab-case")]
262pub enum TriggerKind {
263 Webhook,
264 Cron,
265 Poll,
266 Stream,
267 Predicate,
268 A2aPush,
269}
270
271#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
272pub struct TriggerMatchExpr {
273 #[serde(default)]
274 pub events: Vec<String>,
275 #[serde(flatten, default)]
276 pub extra: BTreeMap<String, toml::Value>,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub struct TriggerRetrySpec {
281 #[serde(default)]
282 pub max: u32,
283 #[serde(default)]
284 pub backoff: TriggerRetryBackoff,
285 #[serde(default = "default_trigger_retention_days")]
286 pub retention_days: u32,
287}
288
289#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
290#[serde(rename_all = "kebab-case")]
291pub enum TriggerRetryBackoff {
292 #[default]
293 Immediate,
294 Svix,
295}
296
297pub(crate) fn default_trigger_retention_days() -> u32 {
298 harn_vm::DEFAULT_INBOX_RETENTION_DAYS
299}
300
301impl Default for TriggerRetrySpec {
302 fn default() -> Self {
303 Self {
304 max: 0,
305 backoff: TriggerRetryBackoff::default(),
306 retention_days: default_trigger_retention_days(),
307 }
308 }
309}
310
311#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "lowercase")]
313pub enum TriggerDispatchPriority {
314 High,
315 #[default]
316 Normal,
317 Low,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(untagged)]
322pub enum TriggerPriorityField {
323 Dispatch(TriggerDispatchPriority),
324 Flow(TriggerPriorityManifestSpec),
325}
326
327#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
328pub struct TriggerBudgetSpec {
329 #[serde(default)]
330 pub max_cost_usd: Option<f64>,
331 #[serde(default, alias = "tokens_max")]
332 pub max_tokens: Option<u64>,
333 #[serde(default)]
334 pub daily_cost_usd: Option<f64>,
335 #[serde(default)]
336 pub hourly_cost_usd: Option<f64>,
337 #[serde(default)]
338 pub max_autonomous_decisions_per_hour: Option<u64>,
339 #[serde(default)]
340 pub max_autonomous_decisions_per_day: Option<u64>,
341 #[serde(default)]
342 pub max_concurrent: Option<u32>,
343 #[serde(default)]
344 pub on_budget_exhausted: harn_vm::TriggerBudgetExhaustionStrategy,
345}
346
347#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
348pub struct TriggerWhenBudgetSpec {
349 #[serde(default)]
350 pub max_cost_usd: Option<f64>,
351 #[serde(default)]
352 pub tokens_max: Option<u64>,
353 #[serde(default)]
354 pub timeout: Option<String>,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358pub struct TriggerConcurrencyManifestSpec {
359 #[serde(default)]
360 pub key: Option<String>,
361 pub max: u32,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
365pub struct TriggerThrottleManifestSpec {
366 #[serde(default)]
367 pub key: Option<String>,
368 pub period: String,
369 pub max: u32,
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub struct TriggerRateLimitManifestSpec {
374 #[serde(default)]
375 pub key: Option<String>,
376 pub period: String,
377 pub max: u32,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381pub struct TriggerDebounceManifestSpec {
382 pub key: String,
383 pub period: String,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387pub struct TriggerSingletonManifestSpec {
388 #[serde(default)]
389 pub key: Option<String>,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
393pub struct TriggerBatchManifestSpec {
394 #[serde(default)]
395 pub key: Option<String>,
396 pub size: u32,
397 pub timeout: String,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct TriggerPriorityManifestSpec {
402 pub key: String,
403 #[serde(default)]
404 pub order: Vec<String>,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
408#[serde(rename_all = "kebab-case")]
409pub enum TriggerStreamWindowMode {
410 Tumbling,
411 Sliding,
412 Session,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
416pub struct TriggerStreamWindowManifestSpec {
417 pub mode: TriggerStreamWindowMode,
418 #[serde(default)]
419 pub key: Option<String>,
420 #[serde(default)]
421 pub size: Option<String>,
422 #[serde(default)]
423 pub every: Option<String>,
424 #[serde(default)]
425 pub gap: Option<String>,
426 #[serde(default)]
427 pub max_items: Option<u32>,
428}
429
430#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
431pub struct TriggerDlqAlertManifestSpec {
432 #[serde(default)]
433 pub destinations: Vec<TriggerDlqAlertDestination>,
434 #[serde(default)]
435 pub threshold: TriggerDlqAlertThreshold,
436}
437
438#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
439pub struct TriggerDlqAlertThreshold {
440 #[serde(default, alias = "entries-in-1h")]
441 pub entries_in_1h: Option<u32>,
442 #[serde(default, alias = "percent-of-dispatches")]
443 pub percent_of_dispatches: Option<f64>,
444}
445
446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447#[serde(tag = "kind", rename_all = "snake_case")]
448pub enum TriggerDlqAlertDestination {
449 Slack {
450 channel: String,
451 #[serde(default)]
452 webhook_url_env: Option<String>,
453 },
454 Email {
455 address: String,
456 },
457 Webhook {
458 url: String,
459 #[serde(default)]
460 headers: BTreeMap<String, String>,
461 },
462}
463
464impl TriggerDlqAlertDestination {
465 pub fn label(&self) -> String {
466 match self {
467 Self::Slack { channel, .. } => format!("slack:{channel}"),
468 Self::Email { address } => format!("email:{address}"),
469 Self::Webhook { url, .. } => format!("webhook:{url}"),
470 }
471 }
472}
473
474#[derive(Debug, Clone, PartialEq, Eq)]
475pub enum TriggerHandlerUri {
476 Local(TriggerFunctionRef),
477 A2a {
478 target: String,
479 allow_cleartext: bool,
480 },
481 Worker {
482 queue: String,
483 },
484 Persona {
485 name: String,
486 },
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct TriggerFunctionRef {
491 pub raw: String,
492 pub module_name: Option<String>,
493 pub function_name: String,
494}
495
496#[derive(Debug, Default, Clone, Deserialize)]
498#[allow(dead_code)] pub struct SkillsConfig {
500 #[serde(default)]
504 pub paths: Vec<String>,
505 #[serde(default)]
510 pub lookup_order: Vec<String>,
511 #[serde(default)]
513 pub disable: Vec<String>,
514 #[serde(default)]
517 pub signer_registry_url: Option<String>,
518 #[serde(default)]
522 pub defaults: SkillDefaults,
523}
524
525#[derive(Debug, Default, Clone, Deserialize)]
526#[allow(dead_code)] pub struct SkillDefaults {
528 #[serde(default)]
529 pub tool_search: Option<String>,
530 #[serde(default)]
531 pub always_loaded: Vec<String>,
532}
533
534#[derive(Debug, Default, Clone, Deserialize)]
536pub struct SkillTables {
537 #[serde(default, rename = "source")]
538 pub sources: Vec<SkillSourceEntry>,
539}
540
541#[derive(Debug, Clone, Deserialize)]
545#[serde(tag = "type", rename_all = "lowercase")]
546#[allow(dead_code)] pub enum SkillSourceEntry {
548 Fs {
549 path: String,
550 #[serde(default)]
551 namespace: Option<String>,
552 },
553 Git {
554 url: String,
555 #[serde(default)]
556 tag: Option<String>,
557 #[serde(default)]
558 namespace: Option<String>,
559 },
560 Registry {
561 #[serde(default)]
562 url: Option<String>,
563 #[serde(default)]
564 name: Option<String>,
565 },
566}
567
568#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
574pub enum PreflightSeverity {
575 #[default]
576 Error,
577 Warning,
578 Off,
579}
580
581impl PreflightSeverity {
582 pub fn from_opt(raw: Option<&str>) -> Self {
583 match raw.map(|s| s.to_ascii_lowercase()) {
584 Some(v) if v == "warning" || v == "warn" => Self::Warning,
585 Some(v) if v == "off" || v == "allow" || v == "silent" => Self::Off,
586 _ => Self::Error,
587 }
588 }
589}
590
591#[derive(Debug, Default, Clone, Deserialize)]
592pub struct CheckConfig {
593 #[serde(default)]
594 pub strict: bool,
595 #[serde(default)]
596 pub strict_types: bool,
597 #[serde(default)]
598 pub disable_rules: Vec<String>,
599 #[serde(default)]
600 pub host_capabilities: HashMap<String, Vec<String>>,
601 #[serde(default, alias = "host_capabilities_file")]
602 pub host_capabilities_path: Option<String>,
603 #[serde(default)]
604 pub bundle_root: Option<String>,
605 #[serde(default, alias = "preflight-severity")]
608 pub preflight_severity: Option<String>,
609 #[serde(default, alias = "preflight-allow")]
613 pub preflight_allow: Vec<String>,
614}
615
616#[derive(Debug, Default, Clone, Deserialize)]
617pub struct WorkspaceConfig {
618 #[serde(default)]
621 pub pipelines: Vec<String>,
622}
623
624#[derive(Debug, Default, Clone, Deserialize)]
625pub struct PackageRegistryConfig {
626 #[serde(default)]
628 pub url: Option<String>,
629}
630
631#[derive(Debug, Clone, Deserialize)]
632pub struct McpServerConfig {
633 pub name: String,
634 #[serde(default)]
635 pub transport: Option<String>,
636 #[serde(default)]
637 pub command: String,
638 #[serde(default)]
639 pub args: Vec<String>,
640 #[serde(default)]
641 pub env: HashMap<String, String>,
642 #[serde(default)]
643 pub url: String,
644 #[serde(default)]
645 pub auth_token: Option<String>,
646 #[serde(default)]
647 pub auth: Option<McpAuthConfig>,
648 #[serde(default)]
649 pub client_id: Option<String>,
650 #[serde(default)]
651 pub client_secret: Option<String>,
652 #[serde(default)]
653 pub scopes: Option<String>,
654 #[serde(default)]
655 pub protocol_version: Option<String>,
656 #[serde(default)]
657 pub protocol_mode: Option<String>,
658 #[serde(default)]
659 pub proxy_server_name: Option<String>,
660 #[serde(default)]
664 pub lazy: bool,
665 #[serde(default)]
669 pub card: Option<String>,
670 #[serde(default, alias = "keep-alive-ms", alias = "keep_alive")]
674 pub keep_alive_ms: Option<u64>,
675}
676
677#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
678pub struct McpAuthConfig {
679 #[serde(default)]
680 pub mode: Option<harn_vm::mcp_auth::OAuthClientAuthMode>,
681 #[serde(default, alias = "client-id")]
682 pub client_id: Option<String>,
683 #[serde(
684 default,
685 alias = "client_secret_id",
686 alias = "client-secret-id",
687 alias = "client_secret_ref",
688 alias = "client-secret-ref"
689 )]
690 pub client_secret_id: Option<String>,
691 #[serde(
692 default,
693 alias = "secret_id",
694 alias = "secret-id",
695 alias = "token-secret-id"
696 )]
697 pub secret_id: Option<String>,
698 #[serde(default, alias = "scope")]
699 pub scopes: Option<String>,
700 #[serde(default, alias = "token_auth_method", alias = "token-auth-method")]
701 pub token_endpoint_auth_method: Option<String>,
702}
703
704#[derive(Debug, Clone, Deserialize)]
705#[allow(dead_code)] pub struct PackageInfo {
707 pub name: Option<String>,
708 pub version: Option<String>,
709 #[serde(default)]
710 pub evals: Vec<String>,
711 #[serde(default)]
712 pub description: Option<String>,
713 #[serde(default)]
714 pub license: Option<String>,
715 #[serde(default)]
716 pub repository: Option<String>,
717 #[serde(default, alias = "harn_version", alias = "harn_version_range")]
718 pub harn: Option<String>,
719 #[serde(default)]
720 pub docs_url: Option<String>,
721 #[serde(default)]
722 pub provenance: Option<String>,
723 #[serde(default)]
724 pub permissions: Vec<String>,
725 #[serde(default, alias = "host-requirements")]
726 pub host_requirements: Vec<String>,
727 #[serde(default)]
728 pub tools: Vec<PackageToolExport>,
729 #[serde(default)]
730 pub skills: Vec<PackageSkillExport>,
731}
732
733#[derive(Debug, Clone, Deserialize, PartialEq)]
734pub struct PackageToolExport {
735 pub name: String,
736 pub module: String,
737 #[serde(default = "default_package_tool_symbol")]
738 pub symbol: String,
739 #[serde(default)]
740 pub description: Option<String>,
741 #[serde(default)]
742 pub permissions: Vec<String>,
743 #[serde(default, alias = "host-requirements")]
744 pub host_requirements: Vec<String>,
745 #[serde(default, alias = "input-schema")]
746 pub input_schema: Option<toml::Value>,
747 #[serde(default, alias = "output-schema")]
748 pub output_schema: Option<toml::Value>,
749 #[serde(default)]
750 pub annotations: BTreeMap<String, toml::Value>,
751}
752
753pub(crate) fn default_package_tool_symbol() -> String {
754 "tools".to_string()
755}
756
757#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
758pub struct PackageSkillExport {
759 pub name: String,
760 pub path: String,
761 #[serde(default)]
762 pub description: Option<String>,
763 #[serde(default)]
764 pub permissions: Vec<String>,
765 #[serde(default, alias = "host-requirements")]
766 pub host_requirements: Vec<String>,
767}
768
769#[derive(Debug, Clone, Deserialize)]
770#[serde(untagged)]
771pub enum Dependency {
772 Table(Box<DepTable>),
773 Path(String),
774}
775
776#[derive(Debug, Clone, Default, Deserialize)]
777pub struct DepTable {
778 pub git: Option<String>,
779 pub tag: Option<String>,
780 pub rev: Option<String>,
781 pub branch: Option<String>,
782 pub version: Option<String>,
783 pub path: Option<String>,
784 pub package: Option<String>,
785 #[serde(default)]
790 pub registry: Option<String>,
791 #[serde(default, alias = "registry-name")]
794 pub registry_name: Option<String>,
795 #[serde(default, alias = "registry-version")]
797 pub registry_version: Option<String>,
798}
799
800impl Dependency {
801 pub(crate) fn git_url(&self) -> Option<&str> {
802 match self {
803 Dependency::Table(t) => t.git.as_deref(),
804 Dependency::Path(_) => None,
805 }
806 }
807
808 pub(crate) fn rev(&self) -> Option<&str> {
809 match self {
810 Dependency::Table(t) => t.rev.as_deref(),
811 Dependency::Path(_) => None,
812 }
813 }
814
815 pub(crate) fn tag(&self) -> Option<&str> {
816 match self {
817 Dependency::Table(t) => t.tag.as_deref(),
818 Dependency::Path(_) => None,
819 }
820 }
821
822 pub(crate) fn branch(&self) -> Option<&str> {
823 match self {
824 Dependency::Table(t) => t.branch.as_deref(),
825 Dependency::Path(_) => None,
826 }
827 }
828
829 pub(crate) fn version(&self) -> Option<&str> {
830 match self {
831 Dependency::Table(t) => t.version.as_deref(),
832 Dependency::Path(_) => None,
833 }
834 }
835
836 pub(crate) fn requires_git(&self) -> bool {
837 self.git_url().is_some() || self.version().is_some()
838 }
839
840 pub(crate) fn local_path(&self) -> Option<&str> {
841 match self {
842 Dependency::Table(t) => t.path.as_deref(),
843 Dependency::Path(p) => Some(p.as_str()),
844 }
845 }
846
847 pub(crate) fn registry_provenance(&self) -> Option<crate::package::RegistryProvenance> {
848 let Dependency::Table(table) = self else {
849 return None;
850 };
851 let source = table.registry.clone()?;
852 let name = table.registry_name.clone()?;
853 let version = table.registry_version.clone()?;
854 Some(crate::package::RegistryProvenance {
855 source,
856 name,
857 version,
858 provenance_url: None,
859 })
860 }
861}
862
863pub(crate) fn validate_package_alias(alias: &str) -> Result<(), PackageError> {
864 let valid = !alias.is_empty()
865 && alias != "."
866 && alias != ".."
867 && alias
868 .bytes()
869 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'));
870 if valid {
871 Ok(())
872 } else {
873 Err(PackageError::Validation(format!(
874 "invalid dependency alias {alias:?}; use ASCII letters, numbers, '.', '_' or '-'"
875 )))
876 }
877}
878
879pub(crate) fn toml_string_literal(value: &str) -> Result<String, PackageError> {
880 use std::fmt::Write as _;
881
882 let mut encoded = String::with_capacity(value.len() + 2);
883 encoded.push('"');
884 for ch in value.chars() {
885 match ch {
886 '\u{08}' => encoded.push_str("\\b"),
887 '\t' => encoded.push_str("\\t"),
888 '\n' => encoded.push_str("\\n"),
889 '\u{0C}' => encoded.push_str("\\f"),
890 '\r' => encoded.push_str("\\r"),
891 '"' => encoded.push_str("\\\""),
892 '\\' => encoded.push_str("\\\\"),
893 ch if ch <= '\u{1F}' || ch == '\u{7F}' => {
894 write!(&mut encoded, "\\u{:04X}", ch as u32).map_err(|error| {
895 PackageError::Manifest(format!("failed to encode TOML string: {error}"))
896 })?;
897 }
898 ch => encoded.push(ch),
899 }
900 }
901 encoded.push('"');
902 Ok(encoded)
903}
904
905#[derive(Debug, Default, Clone)]
906pub struct RuntimeExtensions {
907 pub root_manifest: Option<Manifest>,
908 pub root_manifest_path: Option<PathBuf>,
909 pub root_manifest_dir: Option<PathBuf>,
910 pub llm: Option<harn_vm::llm_config::ProvidersConfig>,
911 pub capabilities: Option<harn_vm::llm::capabilities::CapabilitiesFile>,
912 pub hooks: Vec<ResolvedHookConfig>,
913 pub triggers: Vec<ResolvedTriggerConfig>,
914 pub handoff_routes: Vec<harn_vm::HandoffRouteConfig>,
915 pub provider_connectors: Vec<ResolvedProviderConnectorConfig>,
916}
917
918#[derive(Debug, Clone, Deserialize)]
919pub struct ProviderManifestEntry {
920 pub id: harn_vm::ProviderId,
921 pub connector: ProviderConnectorManifest,
922 #[serde(default)]
923 pub oauth: Option<ProviderOAuthManifest>,
924 #[serde(default)]
925 pub setup: Option<ProviderSetupManifest>,
926 #[serde(default)]
927 pub capabilities: ConnectorCapabilities,
928}
929
930#[derive(Debug, Clone, Deserialize)]
931pub struct ProviderConnectorManifest {
932 #[serde(default)]
933 pub harn: Option<String>,
934 #[serde(default)]
935 pub rust: Option<String>,
936}
937
938#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
939pub struct ProviderOAuthManifest {
940 #[serde(default, alias = "auth_url", alias = "authorization-endpoint")]
941 pub authorization_endpoint: Option<String>,
942 #[serde(default, alias = "token_url", alias = "token-endpoint")]
943 pub token_endpoint: Option<String>,
944 #[serde(default, alias = "registration_url", alias = "registration-endpoint")]
945 pub registration_endpoint: Option<String>,
946 #[serde(default)]
947 pub resource: Option<String>,
948 #[serde(default, alias = "scope")]
949 pub scopes: Option<String>,
950 #[serde(default, alias = "client-id")]
951 pub client_id: Option<String>,
952 #[serde(default, alias = "client-secret")]
953 pub client_secret: Option<String>,
954 #[serde(default, alias = "token_auth_method", alias = "token-auth-method")]
955 pub token_endpoint_auth_method: Option<String>,
956}
957
958#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
959pub struct ProviderSetupManifest {
960 #[serde(default, alias = "auth-type")]
961 pub auth_type: Option<String>,
962 #[serde(default)]
963 pub flow: Option<String>,
964 #[serde(default, alias = "required-scopes", alias = "scopes")]
965 pub required_scopes: Vec<String>,
966 #[serde(default, alias = "required-secrets")]
967 pub required_secrets: Vec<String>,
968 #[serde(default, alias = "setup-command")]
969 pub setup_command: Vec<String>,
970 #[serde(default, alias = "validation-command")]
971 pub validation_command: Vec<String>,
972 #[serde(default, alias = "health-checks")]
973 pub health_checks: Vec<ConnectorHealthCheckManifest>,
974 #[serde(default)]
975 pub recovery: ConnectorRecoveryCopy,
976 #[serde(flatten, default)]
977 pub extra: BTreeMap<String, toml::Value>,
978}
979
980#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
981pub struct ConnectorHealthCheckManifest {
982 pub id: String,
983 pub kind: String,
984 #[serde(default)]
985 pub command: Vec<String>,
986 #[serde(default)]
987 pub secret: Option<String>,
988 #[serde(default)]
989 pub url: Option<String>,
990}
991
992#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
993pub struct ConnectorRecoveryCopy {
994 #[serde(default, alias = "missing-install")]
995 pub missing_install: Option<String>,
996 #[serde(default, alias = "missing-auth")]
997 pub missing_auth: Option<String>,
998 #[serde(default, alias = "expired-credentials")]
999 pub expired_credentials: Option<String>,
1000 #[serde(default, alias = "revoked-credentials")]
1001 pub revoked_credentials: Option<String>,
1002 #[serde(default, alias = "missing-scopes")]
1003 pub missing_scopes: Option<String>,
1004 #[serde(default, alias = "inaccessible-resource")]
1005 pub inaccessible_resource: Option<String>,
1006 #[serde(default, alias = "transient-provider-outage")]
1007 pub transient_provider_outage: Option<String>,
1008}
1009
1010#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
1011pub struct ConnectorCapabilities {
1012 pub webhook: bool,
1013 pub oauth: bool,
1014 pub rate_limit: bool,
1015 pub pagination: bool,
1016 pub graphql: bool,
1017 pub streaming: bool,
1018}
1019
1020impl ConnectorCapabilities {
1021 pub const FEATURES: [&'static str; 6] = [
1022 "webhook",
1023 "oauth",
1024 "rate_limit",
1025 "pagination",
1026 "graphql",
1027 "streaming",
1028 ];
1029
1030 fn enable(&mut self, feature: &str) -> Result<(), String> {
1031 match normalize_connector_capability(feature).as_str() {
1032 "webhook" => self.webhook = true,
1033 "oauth" => self.oauth = true,
1034 "rate_limit" => self.rate_limit = true,
1035 "pagination" => self.pagination = true,
1036 "graphql" => self.graphql = true,
1037 "streaming" => self.streaming = true,
1038 other => {
1039 return Err(format!(
1040 "unknown connector capability '{feature}' (normalized as '{other}')"
1041 ));
1042 }
1043 }
1044 Ok(())
1045 }
1046}
1047
1048#[derive(Debug, Default, Deserialize)]
1049struct ConnectorCapabilitiesTable {
1050 #[serde(default)]
1051 webhook: bool,
1052 #[serde(default)]
1053 oauth: bool,
1054 #[serde(default, alias = "rate-limit")]
1055 rate_limit: bool,
1056 #[serde(default)]
1057 pagination: bool,
1058 #[serde(default)]
1059 graphql: bool,
1060 #[serde(default)]
1061 streaming: bool,
1062}
1063
1064impl From<ConnectorCapabilitiesTable> for ConnectorCapabilities {
1065 fn from(value: ConnectorCapabilitiesTable) -> Self {
1066 Self {
1067 webhook: value.webhook,
1068 oauth: value.oauth,
1069 rate_limit: value.rate_limit,
1070 pagination: value.pagination,
1071 graphql: value.graphql,
1072 streaming: value.streaming,
1073 }
1074 }
1075}
1076
1077impl<'de> Deserialize<'de> for ConnectorCapabilities {
1078 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1079 where
1080 D: serde::Deserializer<'de>,
1081 {
1082 #[derive(Deserialize)]
1083 #[serde(untagged)]
1084 enum RawConnectorCapabilities {
1085 List(Vec<String>),
1086 Table(ConnectorCapabilitiesTable),
1087 }
1088
1089 match RawConnectorCapabilities::deserialize(deserializer)? {
1090 RawConnectorCapabilities::List(features) => {
1091 let mut capabilities = ConnectorCapabilities::default();
1092 for feature in features {
1093 capabilities
1094 .enable(&feature)
1095 .map_err(serde::de::Error::custom)?;
1096 }
1097 Ok(capabilities)
1098 }
1099 RawConnectorCapabilities::Table(table) => Ok(table.into()),
1100 }
1101 }
1102}
1103
1104pub fn normalize_connector_capability(feature: &str) -> String {
1105 feature.trim().to_lowercase().replace('-', "_")
1106}
1107
1108#[derive(Debug, Clone, Default, Deserialize)]
1109pub struct ConnectorContractConfig {
1110 #[serde(default)]
1111 pub version: Option<u32>,
1112 #[serde(default)]
1113 pub fixtures: Vec<ConnectorContractFixture>,
1114}
1115
1116#[derive(Debug, Clone, Deserialize)]
1117pub struct ConnectorContractFixture {
1118 pub provider: harn_vm::ProviderId,
1119 #[serde(default)]
1120 pub name: Option<String>,
1121 #[serde(default)]
1122 pub kind: Option<String>,
1123 #[serde(default)]
1124 pub headers: BTreeMap<String, String>,
1125 #[serde(default)]
1126 pub query: BTreeMap<String, String>,
1127 #[serde(default)]
1128 pub metadata: Option<toml::Value>,
1129 #[serde(default)]
1130 pub body: Option<String>,
1131 #[serde(default)]
1132 pub body_json: Option<toml::Value>,
1133 #[serde(default)]
1134 pub expect_type: Option<String>,
1135 #[serde(default)]
1136 pub expect_kind: Option<String>,
1137 #[serde(default)]
1138 pub expect_dedupe_key: Option<String>,
1139 #[serde(default)]
1140 pub expect_signature_state: Option<String>,
1141 #[serde(default)]
1142 pub expect_payload_contains: Option<toml::Value>,
1143 #[serde(default)]
1144 pub expect_response_status: Option<u16>,
1145 #[serde(default)]
1146 pub expect_response_body: Option<toml::Value>,
1147 #[serde(default)]
1148 pub expect_event_count: Option<usize>,
1149 #[serde(default)]
1150 pub expect_error_contains: Option<String>,
1151}
1152
1153#[derive(Debug, Clone, PartialEq, Eq)]
1154pub enum ResolvedProviderConnectorKind {
1155 Harn { module: String },
1156 RustBuiltin,
1157 Invalid(String),
1158}
1159
1160#[derive(Debug, Clone)]
1161pub struct ResolvedProviderConnectorConfig {
1162 pub id: harn_vm::ProviderId,
1163 pub manifest_dir: PathBuf,
1164 pub connector: ResolvedProviderConnectorKind,
1165 pub oauth: Option<ProviderOAuthManifest>,
1166 pub setup: Option<ProviderSetupManifest>,
1167}
1168
1169#[derive(Debug, Clone)]
1170pub struct ResolvedHookConfig {
1171 pub event: harn_vm::orchestration::HookEvent,
1172 pub pattern: String,
1173 pub handler: String,
1174 pub manifest_dir: PathBuf,
1175 pub package_name: Option<String>,
1176 pub exports: HashMap<String, String>,
1177}
1178
1179#[derive(Debug, Clone)]
1180#[allow(dead_code)] pub struct ResolvedTriggerConfig {
1182 pub id: String,
1183 pub kind: TriggerKind,
1184 pub provider: harn_vm::ProviderId,
1185 pub autonomy_tier: harn_vm::AutonomyTier,
1186 pub match_: TriggerMatchExpr,
1187 pub when: Option<String>,
1188 pub when_budget: Option<TriggerWhenBudgetSpec>,
1189 pub handler: String,
1190 pub dedupe_key: Option<String>,
1191 pub retry: TriggerRetrySpec,
1192 pub dispatch_priority: TriggerDispatchPriority,
1193 pub budget: TriggerBudgetSpec,
1194 pub concurrency: Option<TriggerConcurrencyManifestSpec>,
1195 pub throttle: Option<TriggerThrottleManifestSpec>,
1196 pub rate_limit: Option<TriggerRateLimitManifestSpec>,
1197 pub debounce: Option<TriggerDebounceManifestSpec>,
1198 pub singleton: Option<TriggerSingletonManifestSpec>,
1199 pub batch: Option<TriggerBatchManifestSpec>,
1200 pub window: Option<TriggerStreamWindowManifestSpec>,
1201 pub priority_flow: Option<TriggerPriorityManifestSpec>,
1202 pub secrets: BTreeMap<String, String>,
1203 pub filter: Option<String>,
1204 pub kind_specific: BTreeMap<String, toml::Value>,
1205 pub manifest_dir: PathBuf,
1206 pub manifest_path: PathBuf,
1207 pub package_name: Option<String>,
1208 pub exports: HashMap<String, String>,
1209 pub table_index: usize,
1210 pub shape_error: Option<String>,
1211}
1212
1213#[derive(Debug, Clone)]
1214#[allow(dead_code)] pub struct CollectedManifestTrigger {
1216 pub config: ResolvedTriggerConfig,
1217 pub handler: CollectedTriggerHandler,
1218 pub when: Option<CollectedTriggerPredicate>,
1219 pub flow_control: harn_vm::TriggerFlowControlConfig,
1220}
1221
1222#[derive(Debug, Clone)]
1223#[allow(dead_code)] pub enum CollectedTriggerHandler {
1225 Local {
1226 reference: TriggerFunctionRef,
1227 closure: Arc<harn_vm::VmClosure>,
1228 },
1229 A2a {
1230 target: String,
1231 allow_cleartext: bool,
1232 },
1233 Worker {
1234 queue: String,
1235 },
1236 Persona {
1237 binding: harn_vm::PersonaRuntimeBinding,
1238 },
1239}
1240
1241#[derive(Debug, Clone)]
1242#[allow(dead_code)] pub struct CollectedTriggerPredicate {
1244 pub reference: TriggerFunctionRef,
1245 pub closure: Arc<harn_vm::VmClosure>,
1246}
1247
1248pub(crate) type ManifestModuleCacheKey = (PathBuf, Option<String>, Option<String>);
1249pub(crate) type ManifestModuleExports = BTreeMap<String, Arc<harn_vm::VmClosure>>;
1250
1251static MANIFEST_PROVIDER_SCHEMA_LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
1252
1253pub(crate) async fn lock_manifest_provider_schemas() -> tokio::sync::MutexGuard<'static, ()> {
1254 MANIFEST_PROVIDER_SCHEMA_LOCK
1255 .get_or_init(|| tokio::sync::Mutex::new(()))
1256 .lock()
1257 .await
1258}
1259
1260pub(crate) fn read_manifest_from_path(path: &Path) -> Result<Manifest, PackageError> {
1261 let content = fs::read_to_string(path).map_err(|error| {
1262 if error.kind() == std::io::ErrorKind::NotFound {
1263 PackageError::Manifest(format!(
1264 "No {} found in {}.",
1265 MANIFEST,
1266 path.parent().unwrap_or_else(|| Path::new(".")).display()
1267 ))
1268 } else {
1269 PackageError::Manifest(format!("failed to read {}: {error}", path.display()))
1270 }
1271 })?;
1272 toml::from_str::<Manifest>(&content).map_err(|error| {
1273 PackageError::Manifest(format!("failed to parse {}: {error}", path.display()))
1274 })
1275}
1276
1277pub(crate) fn write_manifest_content(path: &Path, content: &str) -> Result<(), PackageError> {
1278 harn_vm::atomic_io::atomic_write(path, content.as_bytes()).map_err(|error| {
1279 PackageError::Manifest(format!("failed to write {}: {error}", path.display()))
1280 })
1281}
1282
1283pub(crate) fn absolutize_check_config_paths(
1284 mut config: CheckConfig,
1285 manifest_dir: &Path,
1286) -> CheckConfig {
1287 if let Some(path) = config.host_capabilities_path.clone() {
1288 let candidate = PathBuf::from(&path);
1289 if !candidate.is_absolute() {
1290 config.host_capabilities_path =
1291 Some(manifest_dir.join(candidate).display().to_string());
1292 }
1293 }
1294 if let Some(path) = config.bundle_root.clone() {
1295 let candidate = PathBuf::from(&path);
1296 if !candidate.is_absolute() {
1297 config.bundle_root = Some(manifest_dir.join(candidate).display().to_string());
1298 }
1299 }
1300 config
1301}
1302
1303pub(crate) fn find_nearest_manifest(start: &Path) -> Option<(Manifest, PathBuf)> {
1309 const MAX_PARENT_DIRS: usize = 16;
1310 let base = if start.is_absolute() {
1311 start.to_path_buf()
1312 } else {
1313 std::env::current_dir()
1314 .unwrap_or_else(|_| PathBuf::from("."))
1315 .join(start)
1316 };
1317 let mut cursor: Option<PathBuf> = if base.is_dir() {
1318 Some(base)
1319 } else {
1320 base.parent().map(Path::to_path_buf)
1321 };
1322 let mut steps = 0usize;
1323 while let Some(dir) = cursor {
1324 if steps >= MAX_PARENT_DIRS {
1325 break;
1326 }
1327 steps += 1;
1328 let candidate = dir.join(MANIFEST);
1329 if candidate.is_file() {
1330 match read_manifest_from_path(&candidate) {
1331 Ok(manifest) => return Some((manifest, dir)),
1332 Err(error) => {
1333 eprintln!("warning: {error}");
1334 return None;
1335 }
1336 }
1337 }
1338 if dir.join(".git").exists() {
1339 break;
1340 }
1341 cursor = dir.parent().map(Path::to_path_buf);
1342 }
1343 None
1344}
1345
1346pub fn load_check_config(harn_file: Option<&std::path::Path>) -> CheckConfig {
1350 let anchor = harn_file
1351 .map(Path::to_path_buf)
1352 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1353 if let Some((manifest, dir)) = find_nearest_manifest(&anchor) {
1354 return absolutize_check_config_paths(manifest.check, &dir);
1355 }
1356 CheckConfig::default()
1357}
1358
1359pub fn load_workspace_config(anchor: Option<&Path>) -> Option<(WorkspaceConfig, PathBuf)> {
1363 let anchor = anchor
1364 .map(Path::to_path_buf)
1365 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1366 let (manifest, dir) = find_nearest_manifest(&anchor)?;
1367 Some((manifest.workspace, dir))
1368}
1369
1370pub fn load_package_eval_pack_paths(anchor: Option<&Path>) -> Result<Vec<PathBuf>, PackageError> {
1371 let anchor = anchor
1372 .map(Path::to_path_buf)
1373 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1374 let Some((manifest, dir)) = find_nearest_manifest(&anchor) else {
1375 return Err(PackageError::Manifest(
1376 "no harn.toml found for package eval discovery".to_string(),
1377 ));
1378 };
1379
1380 let declared = manifest
1381 .package
1382 .as_ref()
1383 .map(|package| package.evals.clone())
1384 .unwrap_or_default();
1385 let mut paths = if declared.is_empty() {
1386 let default_pack = dir.join("harn.eval.toml");
1387 if default_pack.is_file() {
1388 vec![default_pack]
1389 } else {
1390 Vec::new()
1391 }
1392 } else {
1393 declared
1394 .iter()
1395 .map(|entry| {
1396 let path = PathBuf::from(entry);
1397 if path.is_absolute() {
1398 path
1399 } else {
1400 dir.join(path)
1401 }
1402 })
1403 .collect()
1404 };
1405 paths.sort();
1406 if paths.is_empty() {
1407 return Err(PackageError::Manifest(
1408 "package declares no eval packs; add [package].evals or harn.eval.toml".to_string(),
1409 ));
1410 }
1411 for path in &paths {
1412 if !path.is_file() {
1413 return Err(PackageError::Manifest(format!(
1414 "eval pack does not exist: {}",
1415 path.display()
1416 )));
1417 }
1418 }
1419 Ok(paths)
1420}
1421
1422#[derive(Debug, Clone)]
1423pub(crate) struct ManifestContext {
1424 pub(crate) manifest: Manifest,
1425 pub(crate) dir: PathBuf,
1426}
1427
1428impl ManifestContext {
1429 pub(crate) fn manifest_path(&self) -> PathBuf {
1430 self.dir.join(MANIFEST)
1431 }
1432
1433 pub(crate) fn lock_path(&self) -> PathBuf {
1434 self.dir.join(LOCK_FILE)
1435 }
1436
1437 pub(crate) fn packages_dir(&self) -> PathBuf {
1438 self.dir.join(PKG_DIR)
1439 }
1440}
1441
1442#[derive(Debug, Clone)]
1443pub(crate) struct PackageWorkspace {
1444 manifest_dir: PathBuf,
1445 cache_dir: Option<PathBuf>,
1446 registry_source: Option<String>,
1447 read_process_env: bool,
1448}
1449
1450impl PackageWorkspace {
1451 pub(crate) fn from_current_dir() -> Result<Self, PackageError> {
1452 let manifest_dir = std::env::current_dir()
1453 .map_err(|error| PackageError::Manifest(format!("failed to read cwd: {error}")))?;
1454 Ok(Self {
1455 manifest_dir,
1456 cache_dir: None,
1457 registry_source: None,
1458 read_process_env: true,
1459 })
1460 }
1461
1462 #[cfg(test)]
1463 pub(crate) fn for_test(
1464 manifest_dir: impl Into<PathBuf>,
1465 cache_dir: impl Into<PathBuf>,
1466 ) -> Self {
1467 Self {
1468 manifest_dir: manifest_dir.into(),
1469 cache_dir: Some(cache_dir.into()),
1470 registry_source: None,
1471 read_process_env: false,
1472 }
1473 }
1474
1475 #[cfg(test)]
1476 pub(crate) fn with_registry_source(mut self, source: impl Into<String>) -> Self {
1477 self.registry_source = Some(source.into());
1478 self
1479 }
1480
1481 pub(crate) fn manifest_dir(&self) -> &Path {
1482 &self.manifest_dir
1483 }
1484
1485 pub(crate) fn load_manifest_context(&self) -> Result<ManifestContext, PackageError> {
1486 let manifest_path = self.manifest_dir.join(MANIFEST);
1487 let manifest = read_manifest_from_path(&manifest_path)?;
1488 Ok(ManifestContext {
1489 manifest,
1490 dir: self.manifest_dir.clone(),
1491 })
1492 }
1493
1494 pub(crate) fn cache_root(&self) -> Result<PathBuf, PackageError> {
1495 if let Some(cache_dir) = &self.cache_dir {
1496 return Ok(cache_dir.clone());
1497 }
1498 if self.read_process_env {
1499 if let Ok(value) = std::env::var(HARN_CACHE_DIR_ENV) {
1500 if !value.trim().is_empty() {
1501 return Ok(PathBuf::from(value));
1502 }
1503 }
1504 }
1505
1506 let home = std::env::var_os("HOME")
1507 .map(PathBuf::from)
1508 .ok_or_else(|| "HOME is not set and HARN_CACHE_DIR was not provided".to_string())?;
1509 if cfg!(target_os = "macos") {
1510 return Ok(home.join("Library/Caches/harn"));
1511 }
1512 if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
1513 return Ok(PathBuf::from(xdg).join("harn"));
1514 }
1515 Ok(home.join(".cache/harn"))
1516 }
1517
1518 pub(crate) fn resolve_registry_source(
1519 &self,
1520 explicit: Option<&str>,
1521 ) -> Result<String, PackageError> {
1522 if let Some(explicit) = explicit.map(str::trim).filter(|value| !value.is_empty()) {
1523 return Ok(explicit.to_string());
1524 }
1525 if let Some(source) = self
1526 .registry_source
1527 .as_deref()
1528 .map(str::trim)
1529 .filter(|value| !value.is_empty())
1530 {
1531 if Url::parse(source).is_ok() || PathBuf::from(source).is_absolute() {
1532 return Ok(source.to_string());
1533 }
1534 return Ok(self.manifest_dir.join(source).display().to_string());
1535 }
1536 if self.read_process_env {
1537 if let Ok(value) = std::env::var(HARN_PACKAGE_REGISTRY_ENV) {
1538 let value = value.trim();
1539 if !value.is_empty() {
1540 return Ok(value.to_string());
1541 }
1542 }
1543 }
1544
1545 if let Some((manifest, manifest_dir)) = find_nearest_manifest(&self.manifest_dir) {
1546 if let Some(raw) = manifest
1547 .registry
1548 .url
1549 .as_deref()
1550 .map(str::trim)
1551 .filter(|value| !value.is_empty())
1552 {
1553 if Url::parse(raw).is_ok() || PathBuf::from(raw).is_absolute() {
1554 return Ok(raw.to_string());
1555 }
1556 return Ok(manifest_dir.join(raw).display().to_string());
1557 }
1558 }
1559
1560 Ok(DEFAULT_PACKAGE_REGISTRY_URL.to_string())
1561 }
1562}
1563
1564#[cfg(test)]
1565mod tests {
1566 use super::*;
1567
1568 #[test]
1569 fn package_eval_pack_paths_use_package_manifest_entries() {
1570 let tmp = tempfile::tempdir().unwrap();
1571 let root = tmp.path();
1572 fs::create_dir_all(root.join(".git")).unwrap();
1573 fs::create_dir_all(root.join("evals")).unwrap();
1574 fs::write(
1575 root.join(MANIFEST),
1576 r#"
1577 [package]
1578 name = "demo"
1579 version = "0.1.0"
1580 evals = ["evals/webhook.toml"]
1581 "#,
1582 )
1583 .unwrap();
1584 fs::write(
1585 root.join("evals/webhook.toml"),
1586 "version = 1\n[[cases]]\nrun = \"run.json\"\n",
1587 )
1588 .unwrap();
1589
1590 let paths = load_package_eval_pack_paths(Some(&root.join("src/main.harn"))).unwrap();
1591
1592 assert_eq!(paths, vec![root.join("evals/webhook.toml")]);
1593 }
1594 #[test]
1595 fn preflight_severity_parsing_accepts_synonyms() {
1596 assert_eq!(
1597 PreflightSeverity::from_opt(Some("warning")),
1598 PreflightSeverity::Warning
1599 );
1600 assert_eq!(
1601 PreflightSeverity::from_opt(Some("WARN")),
1602 PreflightSeverity::Warning
1603 );
1604 assert_eq!(
1605 PreflightSeverity::from_opt(Some("off")),
1606 PreflightSeverity::Off
1607 );
1608 assert_eq!(
1609 PreflightSeverity::from_opt(Some("allow")),
1610 PreflightSeverity::Off
1611 );
1612 assert_eq!(
1613 PreflightSeverity::from_opt(Some("error")),
1614 PreflightSeverity::Error
1615 );
1616 assert_eq!(PreflightSeverity::from_opt(None), PreflightSeverity::Error);
1617 assert_eq!(
1619 PreflightSeverity::from_opt(Some("bogus")),
1620 PreflightSeverity::Error
1621 );
1622 }
1623
1624 #[test]
1625 fn load_check_config_walks_up_from_nested_file() {
1626 let tmp = tempfile::tempdir().unwrap();
1627 let root = tmp.path();
1628 std::fs::create_dir_all(root.join(".git")).unwrap();
1630 fs::write(
1631 root.join(MANIFEST),
1632 r#"
1633 [check]
1634 preflight_severity = "warning"
1635 preflight_allow = ["custom.scan", "runtime.*"]
1636 host_capabilities_path = "./schemas/host-caps.json"
1637
1638 [workspace]
1639 pipelines = ["pipelines", "scripts"]
1640 "#,
1641 )
1642 .unwrap();
1643 let nested = root.join("src").join("deep");
1644 std::fs::create_dir_all(&nested).unwrap();
1645 let harn_file = nested.join("pipeline.harn");
1646 fs::write(&harn_file, "pipeline main() {}\n").unwrap();
1647
1648 let cfg = load_check_config(Some(&harn_file));
1649 assert_eq!(cfg.preflight_severity.as_deref(), Some("warning"));
1650 assert_eq!(cfg.preflight_allow, vec!["custom.scan", "runtime.*"]);
1651 let caps_path = cfg.host_capabilities_path.expect("host caps path");
1652 assert!(
1653 caps_path.ends_with("schemas/host-caps.json")
1654 || caps_path.ends_with("schemas\\host-caps.json"),
1655 "unexpected absolutized path: {caps_path}"
1656 );
1657
1658 let (workspace, manifest_dir) =
1659 load_workspace_config(Some(&harn_file)).expect("workspace manifest");
1660 assert_eq!(workspace.pipelines, vec!["pipelines", "scripts"]);
1661 assert_eq!(manifest_dir, root);
1663 }
1664
1665 #[test]
1666 fn orchestrator_drain_config_parses_defaults_and_overrides() {
1667 let default_manifest: Manifest = toml::from_str(
1668 r#"
1669 [package]
1670 name = "fixture"
1671 "#,
1672 )
1673 .unwrap();
1674 assert_eq!(default_manifest.orchestrator.drain.max_items, 1024);
1675 assert_eq!(default_manifest.orchestrator.drain.deadline_seconds, 30);
1676 assert_eq!(default_manifest.orchestrator.pumps.max_outstanding, 64);
1677
1678 let configured: Manifest = toml::from_str(
1679 r#"
1680 [package]
1681 name = "fixture"
1682
1683 [orchestrator]
1684 drain.max_items = 77
1685 drain.deadline_seconds = 12
1686 pumps.max_outstanding = 3
1687 "#,
1688 )
1689 .unwrap();
1690 assert_eq!(configured.orchestrator.drain.max_items, 77);
1691 assert_eq!(configured.orchestrator.drain.deadline_seconds, 12);
1692 assert_eq!(configured.orchestrator.pumps.max_outstanding, 3);
1693 }
1694
1695 #[test]
1696 fn load_check_config_stops_at_git_boundary() {
1697 let tmp = tempfile::tempdir().unwrap();
1698 fs::write(
1700 tmp.path().join(MANIFEST),
1701 "[check]\npreflight_severity = \"off\"\n",
1702 )
1703 .unwrap();
1704 let project = tmp.path().join("project");
1705 std::fs::create_dir_all(project.join(".git")).unwrap();
1706 let inner = project.join("src");
1707 std::fs::create_dir_all(&inner).unwrap();
1708 let harn_file = inner.join("main.harn");
1709 fs::write(&harn_file, "pipeline main() {}\n").unwrap();
1710 let cfg = load_check_config(Some(&harn_file));
1711 assert!(
1712 cfg.preflight_severity.is_none(),
1713 "must not inherit harn.toml from outside the .git boundary"
1714 );
1715 }
1716}