Skip to main content

harn_cli/package/
manifest.rs

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    ConnectorParameterManifest, ConnectorParameterType, ConnectorProtectedProfileManifest,
19    ConnectorReconciliation, ConnectorRecoveryCopy, ConnectorRedactionTarget,
20    ConnectorServiceManifest, ConnectorSetupConfigurationField, ConnectorTestProfile,
21    ProtectedProfileFieldClass, ProviderManifestEntry, ProviderSetupManifest,
22    ResolvedProviderConnectorConfig,
23};
24
25#[derive(Debug, Clone, Deserialize)]
26pub struct Manifest {
27    pub package: Option<PackageInfo>,
28    #[serde(default)]
29    pub dependencies: HashMap<String, Dependency>,
30    #[serde(default)]
31    pub mcp: Vec<McpServerConfig>,
32    #[serde(default)]
33    pub check: CheckConfig,
34    #[serde(default)]
35    pub workspace: WorkspaceConfig,
36    /// `[registry]` table — lightweight package discovery index configuration.
37    /// The CLI also honors `HARN_PACKAGE_REGISTRY` and `--registry` overrides.
38    #[serde(default)]
39    pub registry: PackageRegistryConfig,
40    /// `[skills]` table — per-project skill discovery configuration
41    /// (paths, lookup_order, disable).
42    #[serde(default)]
43    pub skills: SkillsConfig,
44    /// `[[skill.source]]` array-of-tables — declared skill sources
45    /// (filesystem, git, reserved registry).
46    #[serde(default)]
47    pub skill: SkillTables,
48    /// `[capabilities]` section — per-provider-per-model override of
49    /// the shipped capability matrix (`defer_loading`, `tool_search`,
50    /// `prompt_caching`, etc.). Entries under `[[capabilities.provider.<name>]]`
51    /// are prepended to the built-in rules for the same provider so
52    /// early adopters can flag proxied endpoints as supporting tool
53    /// search without waiting for a Harn release. See
54    /// `harn_vm::llm::capabilities` for the rule schema.
55    #[serde(default)]
56    pub capabilities: Option<harn_vm::llm::capabilities::CapabilitiesFile>,
57    /// Stable exported package modules. Keys are the logical import
58    /// suffixes (e.g. `providers/openai`) and values are package-root-
59    /// relative file paths. Consumers import them via `<package>/<key>`.
60    #[serde(default)]
61    pub exports: HashMap<String, String>,
62    /// `[llm]` section — packaged provider definitions, aliases,
63    /// inference rules, tier rules, and model defaults. Uses the same
64    /// schema as `providers.toml`, but merges into the current run
65    /// instead of replacing the global config file.
66    #[serde(default)]
67    pub llm: harn_vm::llm_config::ProvidersConfig,
68    /// `[[hooks]]` array-of-tables — declarative runtime hooks installed
69    /// once per process/thread before execution starts. Matches the
70    /// manifest-extension ABI shape added by `[exports]` / `[llm]`, but
71    /// the handlers themselves live in Harn modules.
72    #[serde(default)]
73    pub hooks: Vec<HookConfig>,
74    /// `[[triggers]]` array-of-tables — declarative event-driven trigger
75    /// registrations that resolve local handlers and predicates from Harn
76    /// modules at load time and preserve remote URI schemes for later
77    /// dispatcher work.
78    #[serde(default)]
79    pub triggers: Vec<TriggerManifestEntry>,
80    /// `[[handoff_routes]]` array-of-tables — declarative handoff route data.
81    /// Route selection stays in Harn stdlib/persona code; the Rust manifest
82    /// loader makes these tenant routes available to that code.
83    #[serde(default)]
84    pub handoff_routes: Vec<harn_vm::HandoffRouteConfig>,
85    /// `[[providers]]` array-of-tables — provider-specific connector
86    /// overrides used by the orchestrator to load either builtin Rust
87    /// connectors or `.harn` modules as connector implementations.
88    #[serde(default)]
89    pub providers: Vec<ProviderManifestEntry>,
90    /// `[[personas]]` array-of-tables — durable, non-executing agent role
91    /// manifests. Personas bind an entry workflow to tools, capabilities,
92    /// autonomy, budgets, receipts, handoffs, evals, and rollout metadata.
93    #[serde(default)]
94    pub personas: Vec<PersonaManifestEntry>,
95    /// `[connector_contract]` table — deterministic package-local fixtures
96    /// consumed by `harn connector check` for pure-Harn connector packages.
97    #[serde(default, alias = "connector-contract")]
98    pub connector_contract: ConnectorContractConfig,
99    /// `[orchestrator]` table — listener-level controls shared by
100    /// manifest-driven ingress surfaces.
101    #[serde(default)]
102    pub orchestrator: OrchestratorConfig,
103    /// `[rules]` table — `sgconfig`-style structural-rule discovery. Lists the
104    /// directories `harn scan` / `harn codemod` load rules from when no
105    /// explicit `--rule`/`--rule-pack` is given.
106    #[serde(default)]
107    pub rules: RulesConfig,
108    /// `[[contributes]]` array-of-tables — host-surface extension
109    /// contributions (editor languages, preview panes, build profiles,
110    /// commands, themes, …). Harn treats `kind` as a host-owned, namespaced
111    /// string and validates only the envelope plus that each contribution's
112    /// declared `scopes` are covered by `[package].permissions`; the host
113    /// (e.g. a host) interprets the kind-specific payload. New contribution
114    /// kinds therefore need no Harn release. This is the editor-layer twin of
115    /// the agent-layer blocks (`[[providers]]`, `[[personas]]`, `[[hooks]]`):
116    /// one signed package may populate any mix of both.
117    #[serde(default)]
118    pub contributes: Vec<ContributionEntry>,
119}
120
121/// A single `[[contributes]]` host-surface contribution.
122///
123/// ```toml
124/// [[contributes]]
125/// kind = "editor.language"          # host-owned namespaced vocabulary
126/// id = "latex"                      # unique within the package
127/// title = "LaTeX"
128/// when = "*.tex"                    # optional activation predicate (host-interpreted)
129/// scopes = ["workspace:read_text"]  # MUST be a subset of [package].permissions
130/// platforms = ["macos", "linux"]    # optional support/parity matrix; empty = all
131/// # kind-specific keys are captured into `config` and interpreted by the host:
132/// languageId = "latex"
133/// extensions = [".tex", ".sty"]
134/// ```
135#[derive(Debug, Clone, Deserialize, Serialize)]
136pub struct ContributionEntry {
137    /// Host-owned, namespaced contribution kind (e.g. `editor.language`,
138    /// `editor.preview`, `build.profile`, `editor.command`, `editor.theme`).
139    /// Harn does not enumerate kinds — new ones need no release; it only
140    /// requires the value be namespaced (`segment(.segment)+`).
141    pub kind: String,
142    /// Stable identifier, unique across the package's contributions.
143    pub id: String,
144    #[serde(default)]
145    pub title: Option<String>,
146    /// Optional activation predicate the host interprets (a glob, a
147    /// `languageId`, or a host-defined expression). Absent = always available.
148    #[serde(default)]
149    pub when: Option<String>,
150    /// Capability scopes this contribution exercises. Every entry MUST be
151    /// declared in `[package].permissions`; validation fails closed otherwise.
152    #[serde(default)]
153    pub scopes: Vec<String>,
154    /// Optional support/parity matrix — which surfaces/platforms this
155    /// contribution targets (e.g. `macos`, `linux`, `windows`, `ide`, `tui`).
156    /// Empty means "all".
157    #[serde(default)]
158    pub platforms: Vec<String>,
159    /// Kind-specific payload, captured verbatim and interpreted by the host.
160    #[serde(flatten)]
161    pub config: BTreeMap<String, toml::Value>,
162}
163
164impl ContributionEntry {
165    /// `true` when `kind` is a non-empty, dot-namespaced identifier such as
166    /// `editor.language`. Single-segment kinds are rejected so third parties
167    /// cannot squat unprefixed names.
168    pub fn has_namespaced_kind(&self) -> bool {
169        let mut segments = 0usize;
170        for segment in self.kind.split('.') {
171            if segment.is_empty()
172                || !segment
173                    .chars()
174                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
175                || !segment.starts_with(|c: char| c.is_ascii_lowercase())
176            {
177                return false;
178            }
179            segments += 1;
180        }
181        segments >= 2
182    }
183}
184
185/// `[rules]` table — project-local structural-rule discovery (#2843).
186///
187/// ```toml
188/// [rules]
189/// ruleDirs = ["rules", "vendor/rules"]
190/// utilDirs = ["rules/util"]
191/// testConfigs = ["rules/tests"]
192/// nativeRuleDirs = ["target/harn-native-rules"]
193/// ```
194///
195/// Paths are resolved relative to the manifest's directory.
196#[derive(Debug, Clone, Default, Deserialize)]
197pub struct RulesConfig {
198    /// Directories of top-level rule `*.toml` files to load.
199    #[serde(default, alias = "rule-dirs", alias = "ruleDirs")]
200    pub rule_dirs: Vec<String>,
201    /// Directories of utility-rule `*.toml` files (referenced via `matches`).
202    #[serde(default, alias = "util-dirs", alias = "utilDirs")]
203    pub util_dirs: Vec<String>,
204    /// Directories holding rule-test fixtures (for `harn rule test`).
205    #[serde(default, alias = "test-configs", alias = "testConfigs")]
206    pub test_configs: Vec<String>,
207    /// Trusted directories of native lint-rule dynamic libraries.
208    #[serde(
209        default,
210        alias = "native-rule-dirs",
211        alias = "nativeRuleDirs",
212        alias = "native_rule_dirs"
213    )]
214    pub native_rule_dirs: Vec<String>,
215}
216
217#[derive(Debug, Clone, Default, Deserialize)]
218pub struct OrchestratorConfig {
219    #[serde(default, alias = "allowed-origins")]
220    pub allowed_origins: Vec<String>,
221    #[serde(default, alias = "max-body-bytes")]
222    pub max_body_bytes: Option<usize>,
223    #[serde(default)]
224    pub budget: OrchestratorBudgetSpec,
225    #[serde(default)]
226    pub drain: OrchestratorDrainConfig,
227    #[serde(default)]
228    pub pumps: OrchestratorPumpConfig,
229}
230
231#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
232pub struct OrchestratorBudgetSpec {
233    #[serde(default)]
234    pub daily_cost_usd: Option<f64>,
235    #[serde(default)]
236    pub hourly_cost_usd: Option<f64>,
237}
238
239#[derive(Debug, Clone, Deserialize)]
240pub struct OrchestratorDrainConfig {
241    #[serde(default = "default_orchestrator_drain_max_items", alias = "max-items")]
242    pub max_items: usize,
243    #[serde(
244        default = "default_orchestrator_drain_deadline_seconds",
245        alias = "deadline-seconds"
246    )]
247    pub deadline_seconds: u64,
248}
249
250impl Default for OrchestratorDrainConfig {
251    fn default() -> Self {
252        Self {
253            max_items: default_orchestrator_drain_max_items(),
254            deadline_seconds: default_orchestrator_drain_deadline_seconds(),
255        }
256    }
257}
258
259pub(crate) fn default_orchestrator_drain_max_items() -> usize {
260    1024
261}
262
263pub(crate) fn default_orchestrator_drain_deadline_seconds() -> u64 {
264    30
265}
266
267#[derive(Debug, Clone, Deserialize)]
268pub struct OrchestratorPumpConfig {
269    #[serde(
270        default = "default_orchestrator_pump_max_outstanding",
271        alias = "max-outstanding"
272    )]
273    pub max_outstanding: usize,
274}
275
276impl Default for OrchestratorPumpConfig {
277    fn default() -> Self {
278        Self {
279            max_outstanding: default_orchestrator_pump_max_outstanding(),
280        }
281    }
282}
283
284pub(crate) fn default_orchestrator_pump_max_outstanding() -> usize {
285    64
286}
287
288#[derive(Debug, Clone, Deserialize)]
289pub struct HookConfig {
290    pub event: harn_vm::orchestration::HookEvent,
291    #[serde(default = "default_hook_pattern")]
292    pub pattern: String,
293    pub handler: String,
294}
295
296pub(crate) fn default_hook_pattern() -> String {
297    "*".to_string()
298}
299
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
301pub struct TriggerManifestEntry {
302    pub id: String,
303    #[serde(default)]
304    pub kind: Option<TriggerKind>,
305    #[serde(default)]
306    pub provider: Option<harn_vm::ProviderId>,
307    #[serde(default, alias = "tier")]
308    pub autonomy_tier: harn_vm::AutonomyTier,
309    #[serde(default, rename = "match")]
310    pub match_: Option<TriggerMatchExpr>,
311    #[serde(default)]
312    pub sources: Vec<TriggerSourceManifestEntry>,
313    #[serde(default)]
314    pub when: Option<String>,
315    #[serde(default)]
316    pub when_budget: Option<TriggerWhenBudgetSpec>,
317    pub handler: String,
318    #[serde(default)]
319    pub dedupe_key: Option<String>,
320    #[serde(default)]
321    pub retry: TriggerRetrySpec,
322    #[serde(default)]
323    pub priority: Option<TriggerPriorityField>,
324    #[serde(default)]
325    pub budget: TriggerBudgetSpec,
326    #[serde(default)]
327    pub concurrency: Option<TriggerConcurrencyManifestSpec>,
328    #[serde(default)]
329    pub throttle: Option<TriggerThrottleManifestSpec>,
330    #[serde(default)]
331    pub rate_limit: Option<TriggerRateLimitManifestSpec>,
332    #[serde(default)]
333    pub debounce: Option<TriggerDebounceManifestSpec>,
334    #[serde(default)]
335    pub singleton: Option<TriggerSingletonManifestSpec>,
336    #[serde(default)]
337    pub batch: Option<TriggerBatchManifestSpec>,
338    #[serde(default)]
339    pub window: Option<TriggerStreamWindowManifestSpec>,
340    #[serde(default, alias = "dlq-alerts")]
341    pub dlq_alerts: Vec<TriggerDlqAlertManifestSpec>,
342    #[serde(default)]
343    pub secrets: BTreeMap<String, String>,
344    #[serde(default)]
345    pub filter: Option<String>,
346    #[serde(flatten, default)]
347    pub kind_specific: BTreeMap<String, toml::Value>,
348}
349
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
351pub struct TriggerSourceManifestEntry {
352    #[serde(default)]
353    pub id: Option<String>,
354    pub kind: TriggerKind,
355    pub provider: harn_vm::ProviderId,
356    #[serde(default, rename = "match")]
357    pub match_: Option<TriggerMatchExpr>,
358    #[serde(default)]
359    pub dedupe_key: Option<String>,
360    #[serde(default)]
361    pub retry: Option<TriggerRetrySpec>,
362    #[serde(default)]
363    pub priority: Option<TriggerPriorityField>,
364    #[serde(default)]
365    pub budget: Option<TriggerBudgetSpec>,
366    #[serde(default)]
367    pub concurrency: Option<TriggerConcurrencyManifestSpec>,
368    #[serde(default)]
369    pub throttle: Option<TriggerThrottleManifestSpec>,
370    #[serde(default)]
371    pub rate_limit: Option<TriggerRateLimitManifestSpec>,
372    #[serde(default)]
373    pub debounce: Option<TriggerDebounceManifestSpec>,
374    #[serde(default)]
375    pub singleton: Option<TriggerSingletonManifestSpec>,
376    #[serde(default)]
377    pub batch: Option<TriggerBatchManifestSpec>,
378    #[serde(default)]
379    pub window: Option<TriggerStreamWindowManifestSpec>,
380    #[serde(default)]
381    pub secrets: BTreeMap<String, String>,
382    #[serde(default)]
383    pub filter: Option<String>,
384    #[serde(flatten, default)]
385    pub kind_specific: BTreeMap<String, toml::Value>,
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
389#[serde(rename_all = "kebab-case")]
390pub enum TriggerKind {
391    Webhook,
392    Cron,
393    Poll,
394    Stream,
395    Predicate,
396    A2aPush,
397}
398
399#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
400pub struct TriggerMatchExpr {
401    #[serde(default)]
402    pub events: Vec<String>,
403    #[serde(flatten, default)]
404    pub extra: BTreeMap<String, toml::Value>,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
408pub struct TriggerRetrySpec {
409    #[serde(default)]
410    pub max: u32,
411    #[serde(default)]
412    pub backoff: TriggerRetryBackoff,
413    #[serde(default = "default_trigger_retention_days")]
414    pub retention_days: u32,
415}
416
417#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
418#[serde(rename_all = "kebab-case")]
419pub enum TriggerRetryBackoff {
420    #[default]
421    Immediate,
422    Svix,
423}
424
425pub(crate) fn default_trigger_retention_days() -> u32 {
426    harn_vm::DEFAULT_INBOX_RETENTION_DAYS
427}
428
429impl Default for TriggerRetrySpec {
430    fn default() -> Self {
431        Self {
432            max: 0,
433            backoff: TriggerRetryBackoff::default(),
434            retention_days: default_trigger_retention_days(),
435        }
436    }
437}
438
439#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
440#[serde(rename_all = "lowercase")]
441pub enum TriggerDispatchPriority {
442    High,
443    #[default]
444    Normal,
445    Low,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449#[serde(untagged)]
450pub enum TriggerPriorityField {
451    Dispatch(TriggerDispatchPriority),
452    Flow(TriggerPriorityManifestSpec),
453}
454
455#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
456pub struct TriggerBudgetSpec {
457    #[serde(default)]
458    pub max_cost_usd: Option<f64>,
459    #[serde(default, alias = "tokens_max")]
460    pub max_tokens: Option<u64>,
461    #[serde(default)]
462    pub daily_cost_usd: Option<f64>,
463    #[serde(default)]
464    pub hourly_cost_usd: Option<f64>,
465    #[serde(default)]
466    pub max_autonomous_decisions_per_hour: Option<u64>,
467    #[serde(default)]
468    pub max_autonomous_decisions_per_day: Option<u64>,
469    #[serde(default)]
470    pub max_concurrent: Option<u32>,
471    #[serde(default)]
472    pub on_budget_exhausted: harn_vm::TriggerBudgetExhaustionStrategy,
473}
474
475#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
476pub struct TriggerWhenBudgetSpec {
477    #[serde(default)]
478    pub max_cost_usd: Option<f64>,
479    #[serde(default)]
480    pub tokens_max: Option<u64>,
481    #[serde(default)]
482    pub timeout: Option<String>,
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
486pub struct TriggerConcurrencyManifestSpec {
487    #[serde(default)]
488    pub key: Option<String>,
489    pub max: u32,
490}
491
492#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
493pub struct TriggerThrottleManifestSpec {
494    #[serde(default)]
495    pub key: Option<String>,
496    pub period: String,
497    pub max: u32,
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct TriggerRateLimitManifestSpec {
502    #[serde(default)]
503    pub key: Option<String>,
504    pub period: String,
505    pub max: u32,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
509pub struct TriggerDebounceManifestSpec {
510    pub key: String,
511    pub period: String,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
515pub struct TriggerSingletonManifestSpec {
516    #[serde(default)]
517    pub key: Option<String>,
518}
519
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
521pub struct TriggerBatchManifestSpec {
522    #[serde(default)]
523    pub key: Option<String>,
524    pub size: u32,
525    pub timeout: String,
526}
527
528#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
529pub struct TriggerPriorityManifestSpec {
530    pub key: String,
531    #[serde(default)]
532    pub order: Vec<String>,
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(rename_all = "kebab-case")]
537pub enum TriggerStreamWindowMode {
538    Tumbling,
539    Sliding,
540    Session,
541}
542
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
544pub struct TriggerStreamWindowManifestSpec {
545    pub mode: TriggerStreamWindowMode,
546    #[serde(default)]
547    pub key: Option<String>,
548    #[serde(default)]
549    pub size: Option<String>,
550    #[serde(default)]
551    pub every: Option<String>,
552    #[serde(default)]
553    pub gap: Option<String>,
554    #[serde(default)]
555    pub max_items: Option<u32>,
556}
557
558#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
559pub struct TriggerDlqAlertManifestSpec {
560    #[serde(default)]
561    pub destinations: Vec<TriggerDlqAlertDestination>,
562    #[serde(default)]
563    pub threshold: TriggerDlqAlertThreshold,
564}
565
566#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
567pub struct TriggerDlqAlertThreshold {
568    #[serde(default, alias = "entries-in-1h")]
569    pub entries_in_1h: Option<u32>,
570    #[serde(default, alias = "percent-of-dispatches")]
571    pub percent_of_dispatches: Option<f64>,
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
575#[serde(tag = "kind", rename_all = "snake_case")]
576pub enum TriggerDlqAlertDestination {
577    Slack {
578        channel: String,
579        #[serde(default)]
580        webhook_url_env: Option<String>,
581    },
582    Email {
583        address: String,
584    },
585    Webhook {
586        url: String,
587        #[serde(default)]
588        headers: BTreeMap<String, String>,
589    },
590}
591
592impl TriggerDlqAlertDestination {
593    pub fn label(&self) -> String {
594        match self {
595            Self::Slack { channel, .. } => format!("slack:{channel}"),
596            Self::Email { address } => format!("email:{address}"),
597            Self::Webhook { url, .. } => format!("webhook:{url}"),
598        }
599    }
600}
601
602#[derive(Debug, Clone, PartialEq, Eq)]
603pub enum TriggerHandlerUri {
604    Local(TriggerFunctionRef),
605    A2a {
606        target: String,
607        allow_cleartext: bool,
608    },
609    Worker {
610        queue: String,
611    },
612    Persona {
613        name: String,
614    },
615    EvalPack {
616        target: String,
617    },
618}
619
620#[derive(Debug, Clone, PartialEq, Eq)]
621pub struct TriggerFunctionRef {
622    pub raw: String,
623    pub module_name: Option<String>,
624    pub function_name: String,
625}
626
627/// `[skills]` table body.
628#[derive(Debug, Default, Clone, Deserialize)]
629#[allow(dead_code)] // `defaults` is parsed per harn#73; default application remains staged.
630pub struct SkillsConfig {
631    /// Additional filesystem roots to scan. Each entry may be a
632    /// literal directory or a glob (`packages/*/skills`). Resolved
633    /// relative to the directory holding harn.toml.
634    #[serde(default)]
635    pub paths: Vec<String>,
636    /// Override priority order. Values are layer labels —
637    /// `cli`, `env`, `project`, `manifest`, `user`, `package`,
638    /// `system`, `host`. Unlisted layers fall through to default
639    /// priority after listed ones.
640    #[serde(default)]
641    pub lookup_order: Vec<String>,
642    /// Disable entire layers. Same label set as `lookup_order`.
643    #[serde(default)]
644    pub disable: Vec<String>,
645    /// Optional remote registry base URL used to resolve
646    /// `<fingerprint>.pub` when a signer is not installed locally.
647    #[serde(default)]
648    pub signer_registry_url: Option<String>,
649    /// `[skills.defaults]` inline sub-table — applied to every
650    /// discovered skill when the field is unset in its SKILL.md
651    /// frontmatter.
652    #[serde(default)]
653    pub defaults: SkillDefaults,
654}
655
656#[derive(Debug, Default, Clone, Deserialize)]
657#[allow(dead_code)] // Parsed per harn#73; loader default application is still staged.
658pub struct SkillDefaults {
659    #[serde(default)]
660    pub tool_search: Option<String>,
661    #[serde(default)]
662    pub always_loaded: Vec<String>,
663}
664
665/// Container for `[[skill.source]]` array-of-tables.
666#[derive(Debug, Default, Clone, Deserialize)]
667pub struct SkillTables {
668    #[serde(default, rename = "source")]
669    pub sources: Vec<SkillSourceEntry>,
670}
671
672/// One `[[skill.source]]` entry. The `registry` variant is accepted
673/// for forward-compat but inert — see issue #73 and `docs/src/skills.md`
674/// for the marketplace timeline.
675#[derive(Debug, Clone, Deserialize)]
676#[serde(tag = "type", rename_all = "lowercase")]
677#[allow(dead_code)] // Git/registry skill sources are manifest-reserved by harn#73.
678pub enum SkillSourceEntry {
679    Fs {
680        path: String,
681        #[serde(default)]
682        namespace: Option<String>,
683    },
684    Git {
685        url: String,
686        #[serde(default)]
687        tag: Option<String>,
688        #[serde(default)]
689        namespace: Option<String>,
690    },
691    Registry {
692        #[serde(default)]
693        url: Option<String>,
694        #[serde(default)]
695        name: Option<String>,
696    },
697}
698
699#[derive(Debug, Default, Clone, Deserialize)]
700pub struct WorkspaceConfig {
701    /// Directory or file globs (repo-relative) that `harn check --workspace`
702    /// walks to collect the full pipeline tree in one invocation.
703    #[serde(default)]
704    pub pipelines: Vec<String>,
705}
706
707#[derive(Debug, Default, Clone, Deserialize)]
708pub struct PackageRegistryConfig {
709    /// URL or filesystem path to a TOML package index.
710    #[serde(default)]
711    pub url: Option<String>,
712}
713
714#[derive(Debug, Clone, Deserialize)]
715pub struct McpServerConfig {
716    pub name: String,
717    #[serde(default)]
718    pub transport: Option<String>,
719    #[serde(default)]
720    pub command: String,
721    #[serde(default)]
722    pub args: Vec<String>,
723    #[serde(default)]
724    pub env: HashMap<String, String>,
725    #[serde(default)]
726    pub url: String,
727    #[serde(default)]
728    pub auth_token: Option<String>,
729    #[serde(default)]
730    pub token_exchange: Option<harn_vm::mcp_oauth::McpTokenExchangeConfig>,
731    #[serde(default)]
732    pub auth: Option<McpAuthConfig>,
733    #[serde(default)]
734    pub client_id: Option<String>,
735    #[serde(default)]
736    pub client_secret: Option<String>,
737    #[serde(default)]
738    pub scopes: Option<String>,
739    #[serde(default)]
740    pub protocol_version: Option<String>,
741    #[serde(default)]
742    pub proxy_server_name: Option<String>,
743    /// When `true`, the server is NOT booted up-front. It boots on the
744    /// first `mcp_call` or on skill activation that declares it in
745    /// `requires_mcp`. See harn#75.
746    #[serde(default)]
747    pub lazy: bool,
748    /// Optional pointer to a Server Card — either an HTTP(S) URL or a
749    /// local filesystem path. When set, `mcp_server_card("name")` reads
750    /// the card from this source (cached per-process with a TTL).
751    #[serde(default)]
752    pub card: Option<String>,
753    /// How long (milliseconds) to keep a lazy server's process alive
754    /// after its last binder releases. 0 / unset → disconnect
755    /// immediately. Ignored for non-lazy servers.
756    #[serde(default, alias = "keep-alive-ms", alias = "keep_alive")]
757    pub keep_alive_ms: Option<u64>,
758}
759
760#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
761pub struct McpAuthConfig {
762    #[serde(default)]
763    pub mode: Option<harn_vm::mcp_auth::OAuthClientAuthMode>,
764    #[serde(default, alias = "client-id")]
765    pub client_id: Option<String>,
766    #[serde(
767        default,
768        alias = "client_secret_id",
769        alias = "client-secret-id",
770        alias = "client_secret_ref",
771        alias = "client-secret-ref"
772    )]
773    pub client_secret_id: Option<String>,
774    #[serde(
775        default,
776        alias = "secret_id",
777        alias = "secret-id",
778        alias = "token-secret-id"
779    )]
780    pub secret_id: Option<String>,
781    #[serde(default, alias = "scope")]
782    pub scopes: Option<String>,
783    #[serde(default, alias = "token_auth_method", alias = "token-auth-method")]
784    pub token_endpoint_auth_method: Option<String>,
785}
786
787#[derive(Debug, Clone, Deserialize)]
788#[allow(dead_code)] // Package metadata feeds authoring/publish validation tracked in harn#471.
789pub struct PackageInfo {
790    pub name: Option<String>,
791    pub version: Option<String>,
792    #[serde(default)]
793    pub evals: Vec<String>,
794    #[serde(default)]
795    pub description: Option<String>,
796    #[serde(default)]
797    pub license: Option<String>,
798    #[serde(default)]
799    pub repository: Option<String>,
800    #[serde(default, alias = "harn_version", alias = "harn_version_range")]
801    pub harn: Option<String>,
802    #[serde(default)]
803    pub docs_url: Option<String>,
804    #[serde(default)]
805    pub provenance: Option<String>,
806    /// Human-facing publisher / developer name shown in marketplace surfaces.
807    #[serde(default)]
808    pub publisher: Option<String>,
809    /// Publisher contact (email or URL) shown alongside the publisher name.
810    #[serde(default)]
811    pub contact: Option<String>,
812    /// Optional ISO-8601 authoring date. Mutation dates are better derived
813    /// from the registry/VCS, but a declared creation date is allowed.
814    #[serde(default)]
815    pub created: Option<String>,
816    #[serde(default)]
817    pub permissions: Vec<String>,
818    #[serde(default, alias = "host-requirements")]
819    pub host_requirements: Vec<String>,
820    #[serde(default)]
821    pub tools: Vec<PackageToolExport>,
822    #[serde(default)]
823    pub skills: Vec<PackageSkillExport>,
824}
825
826#[derive(Debug, Clone, Deserialize, PartialEq)]
827pub struct PackageToolExport {
828    pub name: String,
829    pub module: String,
830    #[serde(default = "default_package_tool_symbol")]
831    pub symbol: String,
832    #[serde(default)]
833    pub description: Option<String>,
834    #[serde(default)]
835    pub permissions: Vec<String>,
836    #[serde(default, alias = "host-requirements")]
837    pub host_requirements: Vec<String>,
838    #[serde(default, alias = "input-schema")]
839    pub input_schema: Option<toml::Value>,
840    #[serde(default, alias = "output-schema")]
841    pub output_schema: Option<toml::Value>,
842    #[serde(default)]
843    pub annotations: BTreeMap<String, toml::Value>,
844}
845
846pub(crate) fn default_package_tool_symbol() -> String {
847    "tools".to_string()
848}
849
850#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
851pub struct PackageSkillExport {
852    pub name: String,
853    pub path: String,
854    #[serde(default)]
855    pub description: Option<String>,
856    #[serde(default)]
857    pub permissions: Vec<String>,
858    #[serde(default, alias = "host-requirements")]
859    pub host_requirements: Vec<String>,
860}
861
862#[derive(Debug, Clone, Deserialize)]
863#[serde(untagged)]
864pub enum Dependency {
865    Table(Box<DepTable>),
866    Path(String),
867}
868
869#[derive(Debug, Clone, Default, Deserialize)]
870pub struct DepTable {
871    pub git: Option<String>,
872    #[serde(default, alias = "archive-url", alias = "archive_url")]
873    pub archive: Option<String>,
874    pub tag: Option<String>,
875    pub rev: Option<String>,
876    pub branch: Option<String>,
877    pub version: Option<String>,
878    pub path: Option<String>,
879    pub package: Option<String>,
880    #[serde(default)]
881    pub checksum: Option<String>,
882    /// Registry index URL/path the dependency was originally added from.
883    /// Persisted in the manifest so registry provenance survives
884    /// round-trips and the lockfile can compare against the registry's
885    /// latest version.
886    #[serde(default)]
887    pub registry: Option<String>,
888    /// Registry-side package name (e.g. `@burin/notion-sdk`). May differ
889    /// from the alias and from the git URL's repo name.
890    #[serde(default, alias = "registry-name")]
891    pub registry_name: Option<String>,
892    /// Registry version specifier the dependency was added against.
893    #[serde(default, alias = "registry-version")]
894    pub registry_version: Option<String>,
895    /// Immutable commit recorded by registry v2 for a Git-backed version.
896    #[serde(default, alias = "registry-commit")]
897    pub registry_commit: Option<String>,
898    /// Registry-v2 evidence URL for the selected published version.
899    #[serde(default, alias = "registry-provenance")]
900    pub registry_provenance: Option<String>,
901}
902
903impl Dependency {
904    pub(crate) fn git_url(&self) -> Option<&str> {
905        match self {
906            Dependency::Table(t) => t.git.as_deref(),
907            Dependency::Path(_) => None,
908        }
909    }
910
911    pub(crate) fn archive_url(&self) -> Option<&str> {
912        match self {
913            Dependency::Table(t) => t.archive.as_deref(),
914            Dependency::Path(_) => None,
915        }
916    }
917
918    pub(crate) fn rev(&self) -> Option<&str> {
919        match self {
920            Dependency::Table(t) => t.rev.as_deref(),
921            Dependency::Path(_) => None,
922        }
923    }
924
925    pub(crate) fn tag(&self) -> Option<&str> {
926        match self {
927            Dependency::Table(t) => t.tag.as_deref(),
928            Dependency::Path(_) => None,
929        }
930    }
931
932    pub(crate) fn branch(&self) -> Option<&str> {
933        match self {
934            Dependency::Table(t) => t.branch.as_deref(),
935            Dependency::Path(_) => None,
936        }
937    }
938
939    pub(crate) fn version(&self) -> Option<&str> {
940        match self {
941            Dependency::Table(t) => t.version.as_deref(),
942            Dependency::Path(_) => None,
943        }
944    }
945
946    pub(crate) fn requires_git(&self) -> bool {
947        self.git_url().is_some()
948    }
949
950    pub(crate) fn local_path(&self) -> Option<&str> {
951        match self {
952            Dependency::Table(t) => t.path.as_deref(),
953            Dependency::Path(p) => Some(p.as_str()),
954        }
955    }
956}
957
958pub(crate) fn validate_package_alias(alias: &str) -> Result<(), PackageError> {
959    if harn_modules::package_snapshot::is_valid_package_name(alias) {
960        Ok(())
961    } else {
962        Err(PackageError::Validation(format!(
963            "invalid dependency alias {alias:?}; use ASCII letters, numbers, '.', '_' or '-'"
964        )))
965    }
966}
967
968pub(crate) fn toml_string_literal(value: &str) -> Result<String, PackageError> {
969    use std::fmt::Write as _;
970
971    let mut encoded = String::with_capacity(value.len() + 2);
972    encoded.push('"');
973    for ch in value.chars() {
974        match ch {
975            '\u{08}' => encoded.push_str("\\b"),
976            '\t' => encoded.push_str("\\t"),
977            '\n' => encoded.push_str("\\n"),
978            '\u{0C}' => encoded.push_str("\\f"),
979            '\r' => encoded.push_str("\\r"),
980            '"' => encoded.push_str("\\\""),
981            '\\' => encoded.push_str("\\\\"),
982            ch if ch <= '\u{1F}' || ch == '\u{7F}' => {
983                write!(&mut encoded, "\\u{:04X}", ch as u32).map_err(|error| {
984                    PackageError::Manifest(format!("failed to encode TOML string: {error}"))
985                })?;
986            }
987            ch => encoded.push(ch),
988        }
989    }
990    encoded.push('"');
991    Ok(encoded)
992}
993#[derive(Debug, Default, Clone)]
994pub struct RuntimeExtensions {
995    pub root_manifest: Option<Manifest>,
996    pub root_manifest_path: Option<PathBuf>,
997    pub root_manifest_dir: Option<PathBuf>,
998    pub(crate) runtime_personas: Vec<ResolvedRuntimePersona>,
999    pub llm: Option<harn_vm::llm_config::ProvidersConfig>,
1000    pub capabilities: Option<harn_vm::llm::capabilities::CapabilitiesFile>,
1001    pub hooks: Vec<ResolvedHookConfig>,
1002    pub triggers: Vec<ResolvedTriggerConfig>,
1003    pub handoff_routes: Vec<harn_vm::HandoffRouteConfig>,
1004    pub provider_connectors: Vec<ResolvedProviderConnectorConfig>,
1005}
1006
1007#[derive(Debug, Clone, Deserialize)]
1008pub struct ProviderConnectorManifest {
1009    #[serde(default)]
1010    pub harn: Option<String>,
1011    #[serde(default)]
1012    pub rust: Option<String>,
1013}
1014
1015#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
1016pub struct ProviderOAuthManifest {
1017    #[serde(default, alias = "auth_url", alias = "authorization-endpoint")]
1018    pub authorization_endpoint: Option<String>,
1019    #[serde(default, alias = "token_url", alias = "token-endpoint")]
1020    pub token_endpoint: Option<String>,
1021    #[serde(default, alias = "registration_url", alias = "registration-endpoint")]
1022    pub registration_endpoint: Option<String>,
1023    #[serde(default)]
1024    pub resource: Option<String>,
1025    #[serde(default, alias = "scope")]
1026    pub scopes: Option<String>,
1027    #[serde(default, alias = "client-id")]
1028    pub client_id: Option<String>,
1029    #[serde(default, alias = "client-secret")]
1030    pub client_secret: Option<String>,
1031    #[serde(default, alias = "token_auth_method", alias = "token-auth-method")]
1032    pub token_endpoint_auth_method: Option<String>,
1033}
1034
1035#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
1036pub struct ConnectorCapabilities {
1037    pub webhook: bool,
1038    pub oauth: bool,
1039    pub rate_limit: bool,
1040    pub pagination: bool,
1041    pub graphql: bool,
1042    pub streaming: bool,
1043}
1044
1045impl ConnectorCapabilities {
1046    pub const FEATURES: [&'static str; 6] = [
1047        "webhook",
1048        "oauth",
1049        "rate_limit",
1050        "pagination",
1051        "graphql",
1052        "streaming",
1053    ];
1054
1055    fn enable(&mut self, feature: &str) -> Result<(), String> {
1056        match normalize_connector_capability(feature).as_str() {
1057            "webhook" => self.webhook = true,
1058            "oauth" => self.oauth = true,
1059            "rate_limit" => self.rate_limit = true,
1060            "pagination" => self.pagination = true,
1061            "graphql" => self.graphql = true,
1062            "streaming" => self.streaming = true,
1063            other => {
1064                return Err(format!(
1065                    "unknown connector capability '{feature}' (normalized as '{other}')"
1066                ));
1067            }
1068        }
1069        Ok(())
1070    }
1071}
1072
1073#[derive(Debug, Default, Deserialize)]
1074struct ConnectorCapabilitiesTable {
1075    #[serde(default)]
1076    webhook: bool,
1077    #[serde(default)]
1078    oauth: bool,
1079    #[serde(default, alias = "rate-limit")]
1080    rate_limit: bool,
1081    #[serde(default)]
1082    pagination: bool,
1083    #[serde(default)]
1084    graphql: bool,
1085    #[serde(default)]
1086    streaming: bool,
1087}
1088
1089impl From<ConnectorCapabilitiesTable> for ConnectorCapabilities {
1090    fn from(value: ConnectorCapabilitiesTable) -> Self {
1091        Self {
1092            webhook: value.webhook,
1093            oauth: value.oauth,
1094            rate_limit: value.rate_limit,
1095            pagination: value.pagination,
1096            graphql: value.graphql,
1097            streaming: value.streaming,
1098        }
1099    }
1100}
1101
1102impl<'de> Deserialize<'de> for ConnectorCapabilities {
1103    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1104    where
1105        D: serde::Deserializer<'de>,
1106    {
1107        #[derive(Deserialize)]
1108        #[serde(untagged)]
1109        enum RawConnectorCapabilities {
1110            List(Vec<String>),
1111            Table(ConnectorCapabilitiesTable),
1112        }
1113
1114        match RawConnectorCapabilities::deserialize(deserializer)? {
1115            RawConnectorCapabilities::List(features) => {
1116                let mut capabilities = ConnectorCapabilities::default();
1117                for feature in features {
1118                    capabilities
1119                        .enable(&feature)
1120                        .map_err(serde::de::Error::custom)?;
1121                }
1122                Ok(capabilities)
1123            }
1124            RawConnectorCapabilities::Table(table) => Ok(table.into()),
1125        }
1126    }
1127}
1128
1129pub fn normalize_connector_capability(feature: &str) -> String {
1130    feature.trim().to_lowercase().replace('-', "_")
1131}
1132
1133#[derive(Debug, Clone, Default, Deserialize)]
1134pub struct ConnectorContractConfig {
1135    #[serde(default)]
1136    pub version: Option<u32>,
1137    #[serde(default)]
1138    pub fixtures: Vec<ConnectorContractFixture>,
1139}
1140
1141#[derive(Debug, Clone, Deserialize)]
1142pub struct ConnectorContractFixture {
1143    pub provider: harn_vm::ProviderId,
1144    #[serde(default)]
1145    pub name: Option<String>,
1146    #[serde(default)]
1147    pub kind: Option<String>,
1148    #[serde(default)]
1149    pub headers: BTreeMap<String, String>,
1150    #[serde(default)]
1151    pub query: BTreeMap<String, String>,
1152    #[serde(default)]
1153    pub metadata: Option<toml::Value>,
1154    #[serde(default)]
1155    pub body: Option<String>,
1156    #[serde(default)]
1157    pub body_json: Option<toml::Value>,
1158    #[serde(default)]
1159    pub expect_type: Option<String>,
1160    #[serde(default)]
1161    pub expect_kind: Option<String>,
1162    #[serde(default)]
1163    pub expect_dedupe_key: Option<String>,
1164    #[serde(default)]
1165    pub expect_signature_state: Option<String>,
1166    #[serde(default)]
1167    pub expect_payload_contains: Option<toml::Value>,
1168    #[serde(default)]
1169    pub expect_response_status: Option<u16>,
1170    #[serde(default)]
1171    pub expect_response_body: Option<toml::Value>,
1172    #[serde(default)]
1173    pub expect_event_count: Option<usize>,
1174    #[serde(default)]
1175    pub expect_error_contains: Option<String>,
1176}
1177
1178#[derive(Debug, Clone, PartialEq, Eq)]
1179pub enum ResolvedProviderConnectorKind {
1180    Harn { module: String },
1181    RustBuiltin,
1182    Invalid(String),
1183}
1184
1185#[derive(Debug, Clone)]
1186pub struct ResolvedHookConfig {
1187    pub event: harn_vm::orchestration::HookEvent,
1188    pub pattern: String,
1189    pub handler: String,
1190    pub manifest_dir: PathBuf,
1191    pub package_name: Option<String>,
1192    pub exports: HashMap<String, String>,
1193}
1194
1195#[derive(Debug, Clone)]
1196pub struct ResolvedTriggerConfig {
1197    pub id: String,
1198    pub kind: TriggerKind,
1199    pub provider: harn_vm::ProviderId,
1200    pub autonomy_tier: harn_vm::AutonomyTier,
1201    pub match_: TriggerMatchExpr,
1202    pub when: Option<String>,
1203    pub when_budget: Option<TriggerWhenBudgetSpec>,
1204    pub handler: String,
1205    pub dedupe_key: Option<String>,
1206    pub retry: TriggerRetrySpec,
1207    pub dispatch_priority: TriggerDispatchPriority,
1208    pub budget: TriggerBudgetSpec,
1209    pub concurrency: Option<TriggerConcurrencyManifestSpec>,
1210    pub throttle: Option<TriggerThrottleManifestSpec>,
1211    pub rate_limit: Option<TriggerRateLimitManifestSpec>,
1212    pub debounce: Option<TriggerDebounceManifestSpec>,
1213    pub singleton: Option<TriggerSingletonManifestSpec>,
1214    pub batch: Option<TriggerBatchManifestSpec>,
1215    pub window: Option<TriggerStreamWindowManifestSpec>,
1216    pub priority_flow: Option<TriggerPriorityManifestSpec>,
1217    pub secrets: BTreeMap<String, String>,
1218    pub filter: Option<String>,
1219    pub kind_specific: BTreeMap<String, toml::Value>,
1220    pub manifest_dir: PathBuf,
1221    pub manifest_path: PathBuf,
1222    pub package_name: Option<String>,
1223    pub exports: HashMap<String, String>,
1224    pub execution_guard: Option<Arc<harn_modules::package_execution::PackageExecutionGuard>>,
1225    pub table_index: usize,
1226    pub shape_error: Option<String>,
1227}
1228
1229#[derive(Debug, Clone)]
1230#[allow(dead_code)] // Collected bindings are validated now and consumed by harn#159 dispatcher work.
1231pub struct CollectedManifestTrigger {
1232    pub config: ResolvedTriggerConfig,
1233    pub handler: CollectedTriggerHandler,
1234    pub when: Option<CollectedTriggerPredicate>,
1235    pub flow_control: harn_vm::TriggerFlowControlConfig,
1236}
1237
1238#[derive(Debug, Clone)]
1239#[allow(dead_code)] // Remote targets and closures are retained for harn#159 trigger execution.
1240pub enum CollectedTriggerHandler {
1241    Local {
1242        reference: TriggerFunctionRef,
1243        callable: harn_vm::VmCallable,
1244    },
1245    A2a {
1246        target: String,
1247        allow_cleartext: bool,
1248    },
1249    Worker {
1250        queue: String,
1251    },
1252    Persona {
1253        binding: harn_vm::PersonaRuntimeBinding,
1254        callable: harn_vm::VmCallable,
1255    },
1256    EvalPack {
1257        target: String,
1258        manifest: Box<harn_vm::orchestration::EvalPackManifest>,
1259        ledger_options: Option<serde_json::Value>,
1260    },
1261}
1262#[derive(Debug, Clone)]
1263#[allow(dead_code)] // Predicate callables are validated now and reused by harn#161 dispatch gating.
1264pub struct CollectedTriggerPredicate {
1265    pub reference: TriggerFunctionRef,
1266    pub callable: harn_vm::VmCallable,
1267}
1268
1269pub(crate) type ManifestModuleCacheKey = (PathBuf, Option<String>, Option<String>);
1270pub(crate) type ManifestModuleExports = BTreeMap<String, Arc<harn_vm::VmClosure>>;
1271
1272static MANIFEST_PROVIDER_SCHEMA_LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
1273
1274pub(crate) async fn lock_manifest_provider_schemas() -> tokio::sync::MutexGuard<'static, ()> {
1275    MANIFEST_PROVIDER_SCHEMA_LOCK
1276        .get_or_init(|| tokio::sync::Mutex::new(()))
1277        .lock()
1278        .await
1279}
1280
1281fn llm_manifest_diagnostics(content: &str) -> Vec<harn_vm::llm_config::ProviderConfigDiagnostic> {
1282    let Ok(value) = toml::from_str::<toml::Value>(content) else {
1283        return Vec::new();
1284    };
1285    let Some(llm) = value.get("llm") else {
1286        return Vec::new();
1287    };
1288    let Ok(llm_src) = toml::to_string(llm) else {
1289        return Vec::new();
1290    };
1291    let Ok(parsed) = harn_vm::llm_config::parse_config_toml_with_diagnostics(&llm_src) else {
1292        return Vec::new();
1293    };
1294    parsed
1295        .diagnostics
1296        .into_iter()
1297        .map(|mut diagnostic| {
1298            if !diagnostic.path.is_empty() {
1299                diagnostic.path = format!("llm.{}", diagnostic.path);
1300            }
1301            diagnostic
1302        })
1303        .collect()
1304}
1305
1306pub(crate) fn read_manifest_from_path(path: &Path) -> Result<Manifest, PackageError> {
1307    let content = fs::read_to_string(path).map_err(|error| {
1308        if error.kind() == std::io::ErrorKind::NotFound {
1309            PackageError::Manifest(format!(
1310                "No {} found in {}.",
1311                MANIFEST,
1312                path.parent().unwrap_or_else(|| Path::new(".")).display()
1313            ))
1314        } else {
1315            PackageError::Manifest(format!("failed to read {}: {error}", path.display()))
1316        }
1317    })?;
1318    let manifest = toml::from_str::<Manifest>(&content).map_err(|error| {
1319        PackageError::Manifest(format!("failed to parse {}: {error}", path.display()))
1320    })?;
1321    for diagnostic in llm_manifest_diagnostics(&content) {
1322        eprintln!("[llm_config] warning in {}: {diagnostic}", path.display());
1323    }
1324    Ok(manifest)
1325}
1326
1327/// Load the `[workspace]` config and the directory of the `harn.toml`
1328/// it came from. Paths in the returned config are left as-is (callers
1329/// resolve them against the returned `manifest_dir`).
1330pub fn load_workspace_config(anchor: Option<&Path>) -> Option<(WorkspaceConfig, PathBuf)> {
1331    let anchor = anchor
1332        .map(Path::to_path_buf)
1333        .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1334    let (manifest, dir) = nearest_manifest_or_warn(&anchor)?;
1335    Some((manifest.workspace, dir))
1336}
1337
1338pub fn load_package_eval_pack_paths(anchor: Option<&Path>) -> Result<Vec<PathBuf>, PackageError> {
1339    let anchor = anchor
1340        .map(Path::to_path_buf)
1341        .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1342    let Some((manifest, dir)) = load_nearest_manifest(&anchor).into_result()? else {
1343        return Err(PackageError::Manifest(
1344            "no harn.toml found for package eval discovery".to_string(),
1345        ));
1346    };
1347
1348    let ctx = ManifestContext { manifest, dir };
1349    let mut paths = eval_pack_paths_from_manifest(&ctx.manifest, &ctx.dir)?;
1350    paths.extend(installed_package_eval_pack_paths(&ctx)?);
1351    paths.sort();
1352    paths.dedup();
1353    if paths.is_empty() {
1354        return Err(PackageError::Manifest(
1355            "package declares no eval packs; add [package].evals, harn.eval.toml, or install a dependency that ships eval packs".to_string(),
1356        ));
1357    }
1358    for path in &paths {
1359        if !path.is_file() {
1360            return Err(PackageError::Manifest(format!(
1361                "eval pack does not exist: {}",
1362                path.display()
1363            )));
1364        }
1365    }
1366    Ok(paths)
1367}
1368
1369fn eval_pack_paths_from_manifest(
1370    manifest: &Manifest,
1371    manifest_dir: &Path,
1372) -> Result<Vec<PathBuf>, PackageError> {
1373    let declared = manifest
1374        .package
1375        .as_ref()
1376        .map(|package| package.evals.clone())
1377        .unwrap_or_default();
1378    let paths = if declared.is_empty() {
1379        let default_pack = manifest_dir.join("harn.eval.toml");
1380        if default_pack.is_file() {
1381            vec![default_pack]
1382        } else {
1383            Vec::new()
1384        }
1385    } else {
1386        declared
1387            .iter()
1388            .map(|entry| {
1389                let path = PathBuf::from(entry);
1390                if path.is_absolute() {
1391                    path
1392                } else {
1393                    manifest_dir.join(path)
1394                }
1395            })
1396            .collect()
1397    };
1398    for path in &paths {
1399        if !path.is_file() {
1400            return Err(PackageError::Manifest(format!(
1401                "eval pack does not exist: {}",
1402                path.display()
1403            )));
1404        }
1405    }
1406    Ok(paths)
1407}
1408
1409fn installed_package_eval_pack_paths(ctx: &ManifestContext) -> Result<Vec<PathBuf>, PackageError> {
1410    let Some(snapshot) = dependency_package_snapshot(&ctx.manifest, &ctx.dir)? else {
1411        return Ok(Vec::new());
1412    };
1413    let lock = LockFile::load(snapshot.lock_path())?.ok_or_else(|| {
1414        PackageError::Lockfile(format!(
1415            "published package generation is missing {}",
1416            snapshot.lock_path().display()
1417        ))
1418    })?;
1419    let mut paths = Vec::new();
1420    let packages_dir = snapshot.packages_root();
1421    for entry in &lock.packages {
1422        validate_package_alias(&entry.name)?;
1423        let package_dir = packages_dir.join(&entry.name);
1424        if package_dir.is_dir() {
1425            if let Some(manifest) = read_package_manifest_from_dir(&package_dir)? {
1426                paths.extend(eval_pack_paths_from_manifest(&manifest, &package_dir)?);
1427            }
1428            continue;
1429        }
1430
1431        let package_file = packages_dir.join(format!("{}.harn", entry.name));
1432        if package_file.is_file() {
1433            continue;
1434        }
1435
1436        return Err(PackageError::Manifest(format!(
1437            "installed package {} is missing under {}; run `harn install`",
1438            entry.name,
1439            packages_dir.display()
1440        )));
1441    }
1442    Ok(paths)
1443}
1444
1445#[derive(Debug, Clone)]
1446pub(crate) struct ManifestContext {
1447    pub(crate) manifest: Manifest,
1448    pub(crate) dir: PathBuf,
1449}
1450
1451impl ManifestContext {
1452    pub(crate) fn manifest_path(&self) -> PathBuf {
1453        self.dir.join(MANIFEST)
1454    }
1455
1456    pub(crate) fn lock_path(&self) -> PathBuf {
1457        self.dir.join(LOCK_FILE)
1458    }
1459}
1460
1461#[cfg(test)]
1462#[path = "manifest_tests.rs"]
1463mod tests;