mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
## Summary PR 3 of 5 in the cloud-managed config client stack. Adds enterprise-managed cloud config as a first-class config layer source. The layer metadata is preserved through config loading, diagnostics, debug output, hook attribution, and app-server protocol surfaces. ## Details - Enterprise-managed config becomes a normal config layer source with backend-supplied `id` and display `name` attached for provenance. - These layers are designed to behave like non-file managed config: they can surface syntax/type diagnostics by layer name even though there is no physical config file. - Relative path settings are resolved from a stored config base so cloud-delivered config remains consistent with existing MDM-delivered config semantics. - Hook attribution distinguishes config-delivered hooks from requirements-delivered hooks via `HookSource::CloudManagedConfig`. - This remains pull-based and snapshot-oriented; the PR adds layer identity/diagnostics, not dynamic reload behavior. ## Validation Validated through the targeted stack checks after rebasing onto current `main`: - Rust crate tests for config/hooks/cloud-config/backend-client/app-server-protocol - Filtered `codex-core` and `codex-app-server` `cloud_config_bundle` tests - Python generated-file contract test - `cargo shear --deny-warnings` - Targeted `argument-comment-lint` for config/hooks
109 lines
3.3 KiB
Rust
109 lines
3.3 KiB
Rust
//! Conversion from cloud-delivered config TOML fragments into config stack layers.
|
|
//!
|
|
//! Backend fragments arrive in backend priority order. This module parses each
|
|
//! fragment, resolves relative path fields against the cloud config base
|
|
//! directory, and returns layers in `ConfigLayerStack` order.
|
|
|
|
use crate::ConfigLayerEntry;
|
|
use crate::ConfigLayerSource;
|
|
use crate::TomlValue;
|
|
use crate::loader::resolve_relative_paths_in_config_toml;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use std::fmt;
|
|
use std::io;
|
|
use thiserror::Error;
|
|
|
|
/// Config fragment delivered by the cloud config bundle.
|
|
///
|
|
/// The bundle orders fragments from highest precedence to lowest precedence.
|
|
/// This module returns config layers in stack order, so callers can append the
|
|
/// result between system and user config without re-sorting.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct CloudConfigFragment {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub contents: String,
|
|
}
|
|
|
|
impl CloudConfigFragment {
|
|
fn source_ref(&self) -> CloudConfigFragmentSource {
|
|
CloudConfigFragmentSource {
|
|
id: self.id.clone(),
|
|
name: self.name.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct CloudConfigFragmentSource {
|
|
pub id: String,
|
|
pub name: String,
|
|
}
|
|
|
|
impl fmt::Display for CloudConfigFragmentSource {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{} ({})", self.name, self.id)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error, PartialEq, Eq)]
|
|
pub enum CloudConfigLayerError {
|
|
#[error("failed to parse cloud config fragment {fragment}: {message}")]
|
|
Parse {
|
|
fragment: CloudConfigFragmentSource,
|
|
message: String,
|
|
},
|
|
#[error("invalid cloud config fragment {fragment}: {message}")]
|
|
Invalid {
|
|
fragment: CloudConfigFragmentSource,
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
pub fn cloud_config_layers_from_fragments(
|
|
fragments: impl IntoIterator<Item = CloudConfigFragment>,
|
|
base_dir: &AbsolutePathBuf,
|
|
) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
|
|
let mut layers = Vec::new();
|
|
for fragment in fragments {
|
|
let source_ref = fragment.source_ref();
|
|
let raw_toml = fragment.contents;
|
|
let value: TomlValue =
|
|
toml::from_str(&raw_toml).map_err(|err| CloudConfigLayerError::Parse {
|
|
fragment: source_ref.clone(),
|
|
message: err.to_string(),
|
|
})?;
|
|
let resolved =
|
|
resolve_relative_paths_in_config_toml(value, base_dir.as_path()).map_err(|err| {
|
|
CloudConfigLayerError::Invalid {
|
|
fragment: source_ref.clone(),
|
|
message: err.to_string(),
|
|
}
|
|
})?;
|
|
layers.push(ConfigLayerEntry::new_with_raw_toml(
|
|
ConfigLayerSource::EnterpriseManaged {
|
|
id: fragment.id,
|
|
name: fragment.name,
|
|
},
|
|
resolved,
|
|
raw_toml,
|
|
base_dir.clone(),
|
|
));
|
|
}
|
|
|
|
// Bundle fragments arrive highest-priority first, while ConfigLayerStack
|
|
// folds lowest-priority to highest-priority.
|
|
layers.reverse();
|
|
Ok(layers)
|
|
}
|
|
|
|
impl From<CloudConfigLayerError> for io::Error {
|
|
fn from(error: CloudConfigLayerError) -> Self {
|
|
io::Error::new(io::ErrorKind::InvalidData, error)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "cloud_config_layers_tests.rs"]
|
|
mod tests;
|