Skip to main content

cfgd_core/composition/
mod.rs

1// Composition — multi-source merge engine with policy enforcement
2// Dependency rules: depends only on config/, errors/. Must NOT import
3// files/, packages/, secrets/, reconciler/, daemon/, providers/.
4
5use std::collections::HashMap;
6
7use crate::config::{
8    ConfigSourcePolicy, EnvVar, ProfileLayer, ResolvedProfile, SourceConstraints, SourceSpec,
9};
10
11mod constraints;
12mod engine;
13mod layers;
14mod merge;
15mod packages;
16mod permissions;
17mod policy;
18mod record;
19
20#[cfg(test)]
21mod tests;
22
23pub use constraints::{check_locked_violations, validate_constraints};
24pub use engine::compose;
25pub use packages::merge_packages;
26pub use permissions::{PermissionChange, detect_permission_changes};
27
28// Re-export sibling submodule items at the parent level so the externalized
29// tests submodule can reach them via `super::*`. The `#[cfg(test)]` guard
30// keeps these at module-private scope and only compiles them when tests run.
31#[cfg(test)]
32use {constraints::*, layers::*, packages::*, permissions::*, policy::*, record::*};
33
34/// Resolution record for conflict reporting.
35#[derive(Debug, Clone)]
36pub struct ConflictResolution {
37    pub resource_id: String,
38    pub resolution_type: ResolutionType,
39    pub winning_source: String,
40    pub details: String,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum ResolutionType {
45    Locked,
46    Required,
47    Override,
48    Rejected,
49    Default,
50}
51
52impl ResolutionType {
53    pub fn label(&self) -> &str {
54        match self {
55            ResolutionType::Locked => "LOCKED",
56            ResolutionType::Required => "REQUIRED",
57            ResolutionType::Override => "OVERRIDE",
58            ResolutionType::Rejected => "REJECTED",
59            ResolutionType::Default => "DEFAULT",
60        }
61    }
62}
63
64/// Input to the composition engine: a source with its resolved profile layers and policy.
65#[derive(Debug)]
66pub struct CompositionInput {
67    pub source_name: String,
68    pub priority: u32,
69    pub policy: ConfigSourcePolicy,
70    pub constraints: SourceConstraints,
71    pub layers: Vec<ProfileLayer>,
72    pub subscription: SubscriptionConfig,
73}
74
75/// Subscription config extracted from the user's cfgd.yaml for this source.
76#[derive(Debug, Clone, Default)]
77pub struct SubscriptionConfig {
78    pub accept_recommended: bool,
79    pub opt_in: Vec<String>,
80    pub overrides: serde_yaml::Value,
81    pub reject: serde_yaml::Value,
82}
83
84impl SubscriptionConfig {
85    pub fn from_spec(spec: &SourceSpec) -> Self {
86        Self {
87            accept_recommended: spec.subscription.accept_recommended,
88            opt_in: spec.subscription.opt_in.clone(),
89            overrides: spec.subscription.overrides.clone(),
90            reject: spec.subscription.reject.clone(),
91        }
92    }
93}
94
95/// Result of composition: merged profile + conflict report.
96#[derive(Debug)]
97pub struct CompositionResult {
98    pub resolved: ResolvedProfile,
99    pub conflicts: Vec<ConflictResolution>,
100    /// Per-source env var sets for template sandboxing.
101    /// Source templates must only access their own env vars + system facts,
102    /// NOT the subscriber's personal env vars.
103    pub source_env: HashMap<String, Vec<EnvVar>>,
104    /// Source name → commit hash, populated by the caller that has access to
105    /// `SourceManager` (not by `compose()` itself, which only sees layers).
106    pub source_commits: HashMap<String, String>,
107}