Skip to main content

codex_config/
cloud_config_layers.rs

1//! Conversion from cloud-delivered config TOML fragments into config stack layers.
2//!
3//! Backend fragments arrive in backend priority order. This module parses each
4//! fragment, resolves relative path fields against the cloud config base
5//! directory, and returns layers in `ConfigLayerStack` order.
6
7use crate::ConfigLayerEntry;
8use crate::ConfigLayerSource;
9use crate::TomlValue;
10use crate::config_toml::ConfigToml;
11use crate::loader::resolve_relative_paths_in_config_toml;
12use crate::strict_config::config_error_from_ignored_toml_value_fields_for_source_name;
13use codex_utils_absolute_path::AbsolutePathBuf;
14use codex_utils_absolute_path::AbsolutePathBufGuard;
15use serde::Deserialize;
16use serde::Serialize;
17use std::fmt;
18use std::io;
19use thiserror::Error;
20
21/// Config fragment delivered by the cloud config bundle.
22///
23/// The bundle orders fragments from highest precedence to lowest precedence.
24/// This module returns config layers in stack order, so callers can append the
25/// result between system and user config without re-sorting.
26#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
27pub struct CloudConfigFragment {
28    pub id: String,
29    pub name: String,
30    pub contents: String,
31}
32
33impl CloudConfigFragment {
34    fn source_ref(&self) -> CloudConfigFragmentSource {
35        CloudConfigFragmentSource {
36            id: self.id.clone(),
37            name: self.name.clone(),
38        }
39    }
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct CloudConfigFragmentSource {
44    pub id: String,
45    pub name: String,
46}
47
48impl fmt::Display for CloudConfigFragmentSource {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "{} ({})", self.name, self.id)
51    }
52}
53
54#[derive(Debug, Error, PartialEq, Eq)]
55pub enum CloudConfigLayerError {
56    #[error("failed to parse cloud config fragment {fragment}: {message}")]
57    Parse {
58        fragment: CloudConfigFragmentSource,
59        message: String,
60    },
61    #[error("invalid cloud config fragment {fragment}: {message}")]
62    Invalid {
63        fragment: CloudConfigFragmentSource,
64        message: String,
65    },
66}
67
68pub fn cloud_config_layers_from_fragments(
69    fragments: impl IntoIterator<Item = CloudConfigFragment>,
70    base_dir: &AbsolutePathBuf,
71) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
72    cloud_config_layers_from_fragments_impl(fragments, base_dir, /*strict_config*/ false)
73}
74
75pub(crate) fn cloud_config_layers_from_fragments_strict(
76    fragments: impl IntoIterator<Item = CloudConfigFragment>,
77    base_dir: &AbsolutePathBuf,
78) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
79    cloud_config_layers_from_fragments_impl(fragments, base_dir, /*strict_config*/ true)
80}
81
82fn cloud_config_layers_from_fragments_impl(
83    fragments: impl IntoIterator<Item = CloudConfigFragment>,
84    base_dir: &AbsolutePathBuf,
85    strict_config: bool,
86) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
87    let mut layers = Vec::new();
88    for fragment in fragments {
89        let source_ref = fragment.source_ref();
90        let raw_toml = fragment.contents;
91        let value: TomlValue =
92            toml::from_str(&raw_toml).map_err(|err| CloudConfigLayerError::Parse {
93                fragment: source_ref.clone(),
94                message: err.to_string(),
95            })?;
96        if strict_config {
97            validate_fragment_strictly(&source_ref, &raw_toml, &value, base_dir)?;
98        }
99        let resolved =
100            resolve_relative_paths_in_config_toml(value, base_dir.as_path()).map_err(|err| {
101                CloudConfigLayerError::Invalid {
102                    fragment: source_ref.clone(),
103                    message: err.to_string(),
104                }
105            })?;
106        layers.push(ConfigLayerEntry::new_with_raw_toml(
107            ConfigLayerSource::EnterpriseManaged {
108                id: fragment.id,
109                name: fragment.name,
110            },
111            resolved,
112            raw_toml,
113            base_dir.clone(),
114        ));
115    }
116
117    // Bundle fragments arrive highest-priority first, while ConfigLayerStack
118    // folds lowest-priority to highest-priority.
119    layers.reverse();
120    Ok(layers)
121}
122
123fn validate_fragment_strictly(
124    source_ref: &CloudConfigFragmentSource,
125    raw_toml: &str,
126    value: &TomlValue,
127    base_dir: &AbsolutePathBuf,
128) -> Result<(), CloudConfigLayerError> {
129    let _guard = AbsolutePathBufGuard::new(base_dir.as_path());
130    if let Some(config_error) = config_error_from_ignored_toml_value_fields_for_source_name::<
131        ConfigToml,
132    >(&source_ref.to_string(), raw_toml, value.clone())
133    {
134        return Err(CloudConfigLayerError::Invalid {
135            fragment: source_ref.clone(),
136            message: config_error.message,
137        });
138    }
139
140    Ok(())
141}
142
143impl From<CloudConfigLayerError> for io::Error {
144    fn from(error: CloudConfigLayerError) -> Self {
145        io::Error::new(io::ErrorKind::InvalidData, error)
146    }
147}
148
149#[cfg(test)]
150#[path = "cloud_config_layers_tests.rs"]
151mod tests;