mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add cloud-managed config layer support (#24620)
## 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
This commit is contained in:
committed by
GitHub
Unverified
parent
20debf746b
commit
8a556296f0
@@ -1012,6 +1012,7 @@ fn analytics_hook_source(source: HookSource) -> &'static str {
|
||||
HookSource::SessionFlags => "session_flags",
|
||||
HookSource::Plugin => "plugin",
|
||||
HookSource::CloudRequirements => "cloud_requirements",
|
||||
HookSource::CloudManagedConfig => "cloud_managed_config",
|
||||
HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
|
||||
HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
|
||||
HookSource::Unknown => "unknown",
|
||||
|
||||
@@ -2002,6 +2002,7 @@
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"cloudManagedConfig",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
|
||||
+28
@@ -7603,6 +7603,33 @@
|
||||
"title": "SystemConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Stable identifier for the delivered layer.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"enterpriseManaged"
|
||||
],
|
||||
"title": "EnterpriseManagedConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "EnterpriseManagedConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||||
"properties": {
|
||||
@@ -10091,6 +10118,7 @@
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"cloudManagedConfig",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
|
||||
+28
@@ -3972,6 +3972,33 @@
|
||||
"title": "SystemConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Stable identifier for the delivered layer.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"enterpriseManaged"
|
||||
],
|
||||
"title": "EnterpriseManagedConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "EnterpriseManagedConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||||
"properties": {
|
||||
@@ -6571,6 +6598,7 @@
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"cloudManagedConfig",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
|
||||
@@ -498,6 +498,33 @@
|
||||
"title": "SystemConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Stable identifier for the delivered layer.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"enterpriseManaged"
|
||||
],
|
||||
"title": "EnterpriseManagedConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "EnterpriseManagedConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||||
"properties": {
|
||||
|
||||
@@ -73,6 +73,33 @@
|
||||
"title": "SystemConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Stable identifier for the delivered layer.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"enterpriseManaged"
|
||||
],
|
||||
"title": "EnterpriseManagedConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "EnterpriseManagedConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
|
||||
"properties": {
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"cloudManagedConfig",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"cloudManagedConfig",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"cloudManagedConfig",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
|
||||
@@ -8,7 +8,17 @@ export type ConfigLayerSource = { "type": "mdm", domain: string, key: string, }
|
||||
* This is the path to the system config.toml file, though it is not
|
||||
* guaranteed to exist.
|
||||
*/
|
||||
file: AbsolutePathBuf, } | { "type": "user",
|
||||
file: AbsolutePathBuf, } | { "type": "enterpriseManaged",
|
||||
/**
|
||||
* Stable identifier for the delivered layer.
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* Admin-facing name for the delivered layer. This is surfaced in
|
||||
* diagnostics so users know which cloud layer needs administrator
|
||||
* attention.
|
||||
*/
|
||||
name: string, } | { "type": "user",
|
||||
/**
|
||||
* This is the path to the user's config.toml file, though it is not
|
||||
* guaranteed to exist.
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "cloudRequirements" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown";
|
||||
export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "cloudRequirements" | "cloudManagedConfig" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown";
|
||||
|
||||
@@ -43,6 +43,19 @@ pub enum ConfigLayerSource {
|
||||
file: AbsolutePathBuf,
|
||||
},
|
||||
|
||||
/// Enterprise-managed config layer delivered by the cloud config bundle.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
EnterpriseManaged {
|
||||
/// Stable identifier for the delivered layer.
|
||||
id: String,
|
||||
|
||||
/// Admin-facing name for the delivered layer. This is surfaced in
|
||||
/// diagnostics so users know which cloud layer needs administrator
|
||||
/// attention.
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// User config layer from $CODEX_HOME/config.toml. This layer is special
|
||||
/// in that it is expected to be:
|
||||
/// - writable by the user
|
||||
@@ -90,6 +103,7 @@ impl ConfigLayerSource {
|
||||
match self {
|
||||
ConfigLayerSource::Mdm { .. } => 0,
|
||||
ConfigLayerSource::System { .. } => 10,
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => 15,
|
||||
ConfigLayerSource::User { profile, .. } => {
|
||||
if profile.is_some() {
|
||||
21
|
||||
|
||||
@@ -48,6 +48,7 @@ v2_enum_from_core!(
|
||||
SessionFlags,
|
||||
Plugin,
|
||||
CloudRequirements,
|
||||
CloudManagedConfig,
|
||||
LegacyManagedConfigFile,
|
||||
LegacyManagedConfigMdm,
|
||||
Unknown,
|
||||
|
||||
@@ -624,6 +624,9 @@ fn override_message(layer: &ConfigLayerSource) -> String {
|
||||
ConfigLayerSource::System { file } => {
|
||||
format!("Overridden by managed config (system): {}", file.display())
|
||||
}
|
||||
ConfigLayerSource::EnterpriseManaged { id: _, name } => {
|
||||
format!("Overridden by enterprise-managed config: {name}")
|
||||
}
|
||||
ConfigLayerSource::Project { dot_codex_folder } => format!(
|
||||
"Overridden by project config: {}/{CONFIG_TOML_FILE}",
|
||||
dot_codex_folder.display(),
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//! 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;
|
||||
@@ -0,0 +1,210 @@
|
||||
use super::*;
|
||||
use crate::CONFIG_TOML_FILE;
|
||||
use crate::ConfigLayerStack;
|
||||
use crate::ConfigLayerStackOrdering;
|
||||
use crate::ConfigRequirements;
|
||||
use crate::ConfigRequirementsToml;
|
||||
use crate::config_toml::ConfigToml;
|
||||
use crate::first_layer_config_error_from_entries;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
|
||||
fn fragment(id: &str, name: &str, contents: &str) -> CloudConfigFragment {
|
||||
CloudConfigFragment {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
contents: contents.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn toml(contents: &str) -> TomlValue {
|
||||
toml::from_str(contents).expect("test TOML should parse")
|
||||
}
|
||||
|
||||
fn base_dir() -> AbsolutePathBuf {
|
||||
test_path_buf("/var/lib/codex").abs()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layers_are_returned_in_stack_order() {
|
||||
let base_dir = base_dir();
|
||||
let layers = cloud_config_layers_from_fragments(
|
||||
vec![
|
||||
fragment("high", "High priority", "model = \"cloud-high\""),
|
||||
fragment("low", "Low priority", "model_provider = \"cloud-low\""),
|
||||
],
|
||||
&base_dir,
|
||||
)
|
||||
.expect("cloud config layers should compose");
|
||||
|
||||
assert_eq!(
|
||||
layers
|
||||
.iter()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
ConfigLayerSource::EnterpriseManaged {
|
||||
id: "low".to_string(),
|
||||
name: "Low priority".to_string(),
|
||||
},
|
||||
ConfigLayerSource::EnterpriseManaged {
|
||||
id: "high".to_string(),
|
||||
name: "High priority".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enterprise_layers_precede_user_and_override_system() {
|
||||
let base_dir = base_dir();
|
||||
let mut layers = vec![ConfigLayerEntry::new(
|
||||
ConfigLayerSource::System {
|
||||
file: test_path_buf("/etc/codex/config.toml").abs(),
|
||||
},
|
||||
toml(
|
||||
r#"
|
||||
model = "system"
|
||||
model_provider = "system"
|
||||
review_model = "system-review"
|
||||
"#,
|
||||
),
|
||||
)];
|
||||
layers.extend(
|
||||
cloud_config_layers_from_fragments(
|
||||
vec![
|
||||
fragment("high", "High priority", "model_provider = \"cloud-high\""),
|
||||
fragment("low", "Low priority", "review_model = \"cloud-low-review\""),
|
||||
],
|
||||
&base_dir,
|
||||
)
|
||||
.expect("cloud config layers should compose"),
|
||||
);
|
||||
layers.push(ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: test_path_buf("/home/alice/.codex/config.toml").abs(),
|
||||
profile: None,
|
||||
},
|
||||
toml("model = \"user\""),
|
||||
));
|
||||
|
||||
let stack = ConfigLayerStack::new(
|
||||
layers,
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("stack should be ordered");
|
||||
|
||||
assert_eq!(
|
||||
stack
|
||||
.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false
|
||||
)
|
||||
.iter()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
ConfigLayerSource::System {
|
||||
file: test_path_buf("/etc/codex/config.toml").abs(),
|
||||
},
|
||||
ConfigLayerSource::EnterpriseManaged {
|
||||
id: "low".to_string(),
|
||||
name: "Low priority".to_string(),
|
||||
},
|
||||
ConfigLayerSource::EnterpriseManaged {
|
||||
id: "high".to_string(),
|
||||
name: "High priority".to_string(),
|
||||
},
|
||||
ConfigLayerSource::User {
|
||||
file: test_path_buf("/home/alice/.codex/config.toml").abs(),
|
||||
profile: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
stack.effective_config(),
|
||||
toml(
|
||||
r#"
|
||||
model = "user"
|
||||
model_provider = "cloud-high"
|
||||
review_model = "cloud-low-review"
|
||||
"#,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_absolute_path_fields_resolve_against_base_dir() {
|
||||
let base_dir = base_dir();
|
||||
let layers = cloud_config_layers_from_fragments(
|
||||
vec![fragment(
|
||||
"cfg_123",
|
||||
"Base policy",
|
||||
"model_instructions_file = \"instructions.md\"",
|
||||
)],
|
||||
&base_dir,
|
||||
)
|
||||
.expect("relative paths should match existing MDM semantics");
|
||||
|
||||
let path = layers[0]
|
||||
.config
|
||||
.get("model_instructions_file")
|
||||
.and_then(TomlValue::as_str)
|
||||
.expect("path should be present");
|
||||
let expected =
|
||||
AbsolutePathBuf::resolve_path_against_base("instructions.md", base_dir.as_path());
|
||||
assert_eq!(path, expected.to_string_lossy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_relative_path_fields_are_allowed_and_resolved() {
|
||||
let base_dir = base_dir();
|
||||
let layers = cloud_config_layers_from_fragments(
|
||||
vec![fragment(
|
||||
"cfg_123",
|
||||
"Base policy",
|
||||
"model_instructions_file = \"~/instructions.md\"",
|
||||
)],
|
||||
&base_dir,
|
||||
)
|
||||
.expect("home-relative paths should be accepted");
|
||||
|
||||
let path = layers[0]
|
||||
.config
|
||||
.get("model_instructions_file")
|
||||
.and_then(TomlValue::as_str)
|
||||
.expect("path should be present");
|
||||
let expected =
|
||||
AbsolutePathBuf::resolve_path_against_base("~/instructions.md", base_dir.as_path());
|
||||
assert_eq!(path, expected.to_string_lossy());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raw_toml_diagnostics_use_enterprise_layer_name() {
|
||||
let base_dir = base_dir();
|
||||
let layers = cloud_config_layers_from_fragments(
|
||||
vec![fragment(
|
||||
"cfg_123",
|
||||
"Base policy",
|
||||
"model_instructions_file = \"instructions.md\"\nmodel = 1",
|
||||
)],
|
||||
&base_dir,
|
||||
)
|
||||
.expect("cloud config layers should parse");
|
||||
|
||||
let error = first_layer_config_error_from_entries::<ConfigToml>(&layers, CONFIG_TOML_FILE)
|
||||
.await
|
||||
.expect("invalid raw TOML should produce a layer diagnostic");
|
||||
|
||||
assert_eq!(
|
||||
error.path,
|
||||
Path::new("enterprise-managed (Base policy, cfg_123)").to_path_buf()
|
||||
);
|
||||
assert_eq!(error.range.start.line, 2);
|
||||
assert_eq!(error.range.start.column, 9);
|
||||
assert!(error.message.contains("invalid type: integer `1`"));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
|
||||
pub fn format_config_layer_source(source: &ConfigLayerSource, config_toml_file: &str) -> String {
|
||||
match source {
|
||||
ConfigLayerSource::Mdm { domain, key } => {
|
||||
format!("MDM ({domain}:{key})")
|
||||
}
|
||||
ConfigLayerSource::System { file } => {
|
||||
format!("system ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::EnterpriseManaged { id, name } => {
|
||||
format!("enterprise-managed ({name}, {id})")
|
||||
}
|
||||
ConfigLayerSource::User { file, .. } => {
|
||||
format!("user ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::Project { dot_codex_folder } => {
|
||||
format!(
|
||||
"project ({}/{config_toml_file})",
|
||||
dot_codex_folder.as_path().display()
|
||||
)
|
||||
}
|
||||
ConfigLayerSource::SessionFlags => "session-flags".to_string(),
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => {
|
||||
format!("legacy managed_config.toml ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
|
||||
"legacy managed_config.toml (MDM)".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::ConfigLayerEntry;
|
||||
use crate::ConfigLayerStack;
|
||||
use crate::ConfigLayerStackOrdering;
|
||||
use crate::format_config_layer_source;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use serde::de::DeserializeOwned;
|
||||
@@ -89,7 +90,6 @@ impl std::error::Error for ConfigLoadError {
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum ConfigDiagnosticSource<'a> {
|
||||
Path(&'a Path),
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
DisplayName(&'a str),
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ impl ConfigDiagnosticSource<'_> {
|
||||
pub(crate) fn to_path_buf(self) -> PathBuf {
|
||||
match self {
|
||||
ConfigDiagnosticSource::Path(path) => path.to_path_buf(),
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
ConfigDiagnosticSource::DisplayName(name) => PathBuf::from(name),
|
||||
}
|
||||
}
|
||||
@@ -201,6 +200,27 @@ where
|
||||
I: IntoIterator<Item = &'a ConfigLayerEntry>,
|
||||
{
|
||||
for layer in layers {
|
||||
if let Some(contents) = layer.raw_toml() {
|
||||
let source_name = format_config_layer_source(&layer.name, config_toml_file);
|
||||
let Some(base_dir) = layer.raw_toml_base_dir() else {
|
||||
tracing::debug!(
|
||||
"Skipping raw TOML diagnostics for {source_name} because it has no base directory"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
// Match the base directory used when the raw non-file layer was
|
||||
// parsed into the runtime layer so diagnostics resolve relative
|
||||
// path fields with the same semantics.
|
||||
let _absolute_path_base = AbsolutePathBufGuard::new(base_dir.as_path());
|
||||
if let Some(error) = config_error_from_typed_toml_for_source::<T>(
|
||||
ConfigDiagnosticSource::DisplayName(&source_name),
|
||||
contents,
|
||||
) {
|
||||
return Some(error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(path) = config_path_for_layer(layer, config_toml_file) else {
|
||||
continue;
|
||||
};
|
||||
@@ -235,6 +255,7 @@ fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Op
|
||||
}
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.to_path_buf()),
|
||||
ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::EnterpriseManaged { .. }
|
||||
| ConfigLayerSource::SessionFlags
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
mod cloud_config_layers;
|
||||
mod cloud_requirements;
|
||||
mod config_layer_source;
|
||||
mod config_requirements;
|
||||
pub mod config_toml;
|
||||
mod constraint;
|
||||
@@ -29,6 +31,10 @@ pub mod types;
|
||||
|
||||
pub const CONFIG_TOML_FILE: &str = "config.toml";
|
||||
|
||||
pub use cloud_config_layers::CloudConfigFragment;
|
||||
pub use cloud_config_layers::CloudConfigFragmentSource;
|
||||
pub use cloud_config_layers::CloudConfigLayerError;
|
||||
pub use cloud_config_layers::cloud_config_layers_from_fragments;
|
||||
pub use cloud_requirements::CloudRequirementsLoadError;
|
||||
pub use cloud_requirements::CloudRequirementsLoadErrorCode;
|
||||
pub use cloud_requirements::CloudRequirementsLoader;
|
||||
@@ -36,6 +42,7 @@ pub use codex_app_server_protocol::ConfigLayerSource;
|
||||
pub use codex_protocol::config_types::ProfileV2Name;
|
||||
pub use codex_protocol::config_types::ProfileV2NameParseError;
|
||||
pub use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
pub use config_layer_source::format_config_layer_source;
|
||||
pub use config_requirements::AppRequirementToml;
|
||||
pub use config_requirements::AppToolRequirementToml;
|
||||
pub use config_requirements::AppToolsRequirementsToml;
|
||||
|
||||
@@ -366,13 +366,18 @@ pub async fn load_config_layers_state(
|
||||
// paths, starting with `./`, but a path starting with `~/` _is_ a
|
||||
// supported use case. Because resolve_relative_paths_in_config_toml()
|
||||
// relies on AbsolutePathBufGuard to resolve `~/`, we must supply a
|
||||
// value for base_dir, so codex_home is as good a value as any.
|
||||
let managed_config =
|
||||
resolve_relative_paths_in_config_toml(config.managed_config, codex_home)?;
|
||||
// value for base_dir. Preserve that same base on the layer so later
|
||||
// raw-TOML diagnostics parse with the same path semantics.
|
||||
let raw_toml_base_dir = AbsolutePathBuf::from_absolute_path(codex_home)?;
|
||||
let managed_config = resolve_relative_paths_in_config_toml(
|
||||
config.managed_config,
|
||||
raw_toml_base_dir.as_path(),
|
||||
)?;
|
||||
layers.push(ConfigLayerEntry::new_with_raw_toml(
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
|
||||
managed_config,
|
||||
config.raw_toml,
|
||||
raw_toml_base_dir,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -97,33 +97,47 @@ impl LoaderOverrides {
|
||||
pub struct ConfigLayerEntry {
|
||||
pub name: ConfigLayerSource,
|
||||
pub config: TomlValue,
|
||||
pub raw_toml: Option<String>,
|
||||
pub version: String,
|
||||
pub disabled_reason: Option<String>,
|
||||
raw_toml: Option<RawTomlLayer>,
|
||||
hooks_config_folder_override: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct RawTomlLayer {
|
||||
contents: String,
|
||||
base_dir: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
impl ConfigLayerEntry {
|
||||
pub fn new(name: ConfigLayerSource, config: TomlValue) -> Self {
|
||||
let version = version_for_toml(&config);
|
||||
Self {
|
||||
name,
|
||||
config,
|
||||
raw_toml: None,
|
||||
version,
|
||||
disabled_reason: None,
|
||||
raw_toml: None,
|
||||
hooks_config_folder_override: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_raw_toml(name: ConfigLayerSource, config: TomlValue, raw_toml: String) -> Self {
|
||||
pub fn new_with_raw_toml(
|
||||
name: ConfigLayerSource,
|
||||
config: TomlValue,
|
||||
raw_toml: String,
|
||||
raw_toml_base_dir: AbsolutePathBuf,
|
||||
) -> Self {
|
||||
let version = version_for_toml(&config);
|
||||
Self {
|
||||
name,
|
||||
config,
|
||||
raw_toml: Some(raw_toml),
|
||||
version,
|
||||
disabled_reason: None,
|
||||
raw_toml: Some(RawTomlLayer {
|
||||
contents: raw_toml,
|
||||
base_dir: raw_toml_base_dir,
|
||||
}),
|
||||
hooks_config_folder_override: None,
|
||||
}
|
||||
}
|
||||
@@ -137,9 +151,9 @@ impl ConfigLayerEntry {
|
||||
Self {
|
||||
name,
|
||||
config,
|
||||
raw_toml: None,
|
||||
version,
|
||||
disabled_reason: Some(disabled_reason.into()),
|
||||
raw_toml: None,
|
||||
hooks_config_folder_override: None,
|
||||
}
|
||||
}
|
||||
@@ -149,7 +163,13 @@ impl ConfigLayerEntry {
|
||||
}
|
||||
|
||||
pub fn raw_toml(&self) -> Option<&str> {
|
||||
self.raw_toml.as_deref()
|
||||
self.raw_toml
|
||||
.as_ref()
|
||||
.map(|raw_toml| raw_toml.contents.as_str())
|
||||
}
|
||||
|
||||
pub fn raw_toml_base_dir(&self) -> Option<&AbsolutePathBuf> {
|
||||
self.raw_toml.as_ref().map(|raw_toml| &raw_toml.base_dir)
|
||||
}
|
||||
|
||||
pub(crate) fn with_hooks_config_folder_override(
|
||||
@@ -181,6 +201,7 @@ impl ConfigLayerEntry {
|
||||
match &self.name {
|
||||
ConfigLayerSource::Mdm { .. } => None,
|
||||
ConfigLayerSource::System { file } => file.parent(),
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => None,
|
||||
ConfigLayerSource::User { file, .. } => file.parent(),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()),
|
||||
ConfigLayerSource::SessionFlags => None,
|
||||
|
||||
@@ -349,6 +349,7 @@ fn skill_roots_from_layer_stack_inner(
|
||||
});
|
||||
}
|
||||
ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::EnterpriseManaged { .. }
|
||||
| ConfigLayerSource::SessionFlags
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {}
|
||||
|
||||
@@ -704,6 +704,7 @@ fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str);
|
||||
HookSource::SessionFlags => "session_flags",
|
||||
HookSource::Plugin => "plugin",
|
||||
HookSource::CloudRequirements => "cloud_requirements",
|
||||
HookSource::CloudManagedConfig => "cloud_managed_config",
|
||||
HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
|
||||
HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
|
||||
HookSource::Unknown => "unknown",
|
||||
|
||||
@@ -371,6 +371,9 @@ fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf {
|
||||
ConfigLayerSource::Mdm { domain, key } => {
|
||||
synthetic_layer_path(&format!("<mdm:{domain}:{key}>/{CONFIG_TOML_FILE}"))
|
||||
}
|
||||
ConfigLayerSource::EnterpriseManaged { id, name } => synthetic_layer_path(&format!(
|
||||
"<enterprise-managed:{name}:{id}>/{CONFIG_TOML_FILE}"
|
||||
)),
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
|
||||
synthetic_layer_path("<legacy-managed-config.toml-mdm>/managed_config.toml")
|
||||
}
|
||||
@@ -611,6 +614,7 @@ fn hook_metadata_for_config_layer_source(source: &ConfigLayerSource) -> (HookSou
|
||||
ConfigLayerSource::User { .. } => (HookSource::User, false),
|
||||
ConfigLayerSource::Project { .. } => (HookSource::Project, false),
|
||||
ConfigLayerSource::Mdm { .. } => (HookSource::Mdm, true),
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => (HookSource::CloudManagedConfig, true),
|
||||
ConfigLayerSource::SessionFlags => (HookSource::SessionFlags, false),
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => {
|
||||
(HookSource::LegacyManagedConfigFile, true)
|
||||
@@ -1059,6 +1063,13 @@ mod tests {
|
||||
}),
|
||||
(HookSource::Mdm, true),
|
||||
);
|
||||
assert_eq!(
|
||||
super::hook_metadata_for_config_layer_source(&ConfigLayerSource::EnterpriseManaged {
|
||||
id: "cfg_123".to_string(),
|
||||
name: "Base policy".to_string(),
|
||||
}),
|
||||
(HookSource::CloudManagedConfig, true),
|
||||
);
|
||||
assert_eq!(
|
||||
super::hook_metadata_for_config_layer_source(&ConfigLayerSource::SessionFlags),
|
||||
(HookSource::SessionFlags, false),
|
||||
|
||||
@@ -1397,6 +1397,7 @@ pub enum HookSource {
|
||||
SessionFlags,
|
||||
Plugin,
|
||||
CloudRequirements,
|
||||
CloudManagedConfig,
|
||||
LegacyManagedConfigFile,
|
||||
LegacyManagedConfigMdm,
|
||||
#[default]
|
||||
|
||||
@@ -778,6 +778,7 @@ fn detail_source_value(hook: &HookMetadata) -> String {
|
||||
HookSource::System
|
||||
| HookSource::Mdm
|
||||
| HookSource::CloudRequirements
|
||||
| HookSource::CloudManagedConfig
|
||||
| HookSource::LegacyManagedConfigFile
|
||||
| HookSource::LegacyManagedConfigMdm => config_source_label(hook.source).to_string(),
|
||||
_ => format!(
|
||||
@@ -797,6 +798,7 @@ fn config_source_label(source: HookSource) -> &'static str {
|
||||
HookSource::SessionFlags => "Session flags",
|
||||
HookSource::Plugin => unreachable!("plugin hooks are handled by summary_source"),
|
||||
HookSource::CloudRequirements => "Admin config",
|
||||
HookSource::CloudManagedConfig => "Cloud-managed config",
|
||||
HookSource::LegacyManagedConfigFile => "Admin config",
|
||||
HookSource::LegacyManagedConfigMdm => "Admin config",
|
||||
HookSource::Unknown => "Unknown source",
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::history_cell::PlainHistoryCell;
|
||||
use crate::legacy_core::config::Config;
|
||||
use crate::session_state::SessionNetworkProxyRuntime;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
@@ -13,6 +14,7 @@ use codex_config::RequirementSource;
|
||||
use codex_config::ResidencyRequirement;
|
||||
use codex_config::SandboxModeRequirement;
|
||||
use codex_config::WebSearchModeRequirement;
|
||||
use codex_config::format_config_layer_source;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use toml::Value as TomlValue;
|
||||
@@ -71,7 +73,7 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
|
||||
lines.push(" <none>".dim().into());
|
||||
} else {
|
||||
for (index, layer) in layers.iter().enumerate() {
|
||||
let source = format_config_layer_source(&layer.name);
|
||||
let source = format_config_layer_source(&layer.name, CONFIG_TOML_FILE);
|
||||
let status = if layer.is_disabled() {
|
||||
"disabled"
|
||||
} else {
|
||||
@@ -267,9 +269,9 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
|
||||
fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>> {
|
||||
match &layer.name {
|
||||
ConfigLayerSource::SessionFlags => render_session_flag_details(&layer.config),
|
||||
ConfigLayerSource::Mdm { .. } | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
|
||||
render_mdm_layer_details(layer)
|
||||
}
|
||||
ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::EnterpriseManaged { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => render_non_file_layer_value(layer),
|
||||
ConfigLayerSource::System { .. }
|
||||
| ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
@@ -308,21 +310,36 @@ fn format_managed_hooks_requirements(hooks: &ManagedHooksRequirementsToml) -> St
|
||||
join_or_empty(parts)
|
||||
}
|
||||
|
||||
fn render_mdm_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>> {
|
||||
fn render_non_file_layer_value(layer: &ConfigLayerEntry) -> Vec<Line<'static>> {
|
||||
let label = non_file_layer_value_label(&layer.name);
|
||||
let value = layer
|
||||
.raw_toml()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| format_toml_value(&layer.config));
|
||||
if value.is_empty() {
|
||||
return vec![" MDM value: <empty>".dim().into()];
|
||||
return vec![format!(" {label}: <empty>").dim().into()];
|
||||
}
|
||||
|
||||
if value.contains('\n') {
|
||||
let mut lines = vec![" MDM value:".into()];
|
||||
let mut lines = vec![format!(" {label}:").into()];
|
||||
lines.extend(value.lines().map(|line| format!(" {line}").into()));
|
||||
lines
|
||||
} else {
|
||||
vec![format!(" MDM value: {value}").into()]
|
||||
vec![format!(" {label}: {value}").into()]
|
||||
}
|
||||
}
|
||||
|
||||
fn non_file_layer_value_label(source: &ConfigLayerSource) -> &'static str {
|
||||
match source {
|
||||
ConfigLayerSource::Mdm { .. } | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
|
||||
"MDM value"
|
||||
}
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => "Enterprise-managed config value",
|
||||
ConfigLayerSource::SessionFlags
|
||||
| ConfigLayerSource::System { .. }
|
||||
| ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => "Layer value",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,33 +405,6 @@ fn normalize_allowed_web_search_modes(
|
||||
normalized
|
||||
}
|
||||
|
||||
fn format_config_layer_source(source: &ConfigLayerSource) -> String {
|
||||
match source {
|
||||
ConfigLayerSource::Mdm { domain, key } => {
|
||||
format!("MDM ({domain}:{key})")
|
||||
}
|
||||
ConfigLayerSource::System { file } => {
|
||||
format!("system ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::User { file, .. } => {
|
||||
format!("user ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::Project { dot_codex_folder } => {
|
||||
format!(
|
||||
"project ({}/config.toml)",
|
||||
dot_codex_folder.as_path().display()
|
||||
)
|
||||
}
|
||||
ConfigLayerSource::SessionFlags => "session-flags".to_string(),
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => {
|
||||
format!("legacy managed_config.toml ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
|
||||
"legacy managed_config.toml (MDM)".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_sandbox_mode_requirement(mode: SandboxModeRequirement) -> String {
|
||||
match mode {
|
||||
SandboxModeRequirement::ReadOnly => "read-only".to_string(),
|
||||
@@ -897,12 +887,18 @@ model = "managed_model"
|
||||
approval_policy = "never"
|
||||
"#;
|
||||
let mdm_value = toml::from_str::<TomlValue>(raw_mdm_toml).expect("MDM value");
|
||||
let mdm_base_dir = if cfg!(windows) {
|
||||
absolute_path("C:\\codex")
|
||||
} else {
|
||||
absolute_path("/var/lib/codex")
|
||||
};
|
||||
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![ConfigLayerEntry::new_with_raw_toml(
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
|
||||
mdm_value,
|
||||
raw_mdm_toml.to_string(),
|
||||
mdm_base_dir,
|
||||
)],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
@@ -917,6 +913,44 @@ approval_policy = "never"
|
||||
assert!(rendered.contains("approval_policy = \"never\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_config_output_shows_enterprise_managed_layer_value() {
|
||||
let raw_cloud_toml = r#"
|
||||
# managed by cloud
|
||||
model = "enterprise_model"
|
||||
approval_policy = "never"
|
||||
"#;
|
||||
let cloud_value = toml::from_str::<TomlValue>(raw_cloud_toml).expect("cloud value");
|
||||
let cloud_base_dir = if cfg!(windows) {
|
||||
absolute_path("C:\\codex")
|
||||
} else {
|
||||
absolute_path("/var/lib/codex")
|
||||
};
|
||||
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![ConfigLayerEntry::new_with_raw_toml(
|
||||
ConfigLayerSource::EnterpriseManaged {
|
||||
id: "cfg_123".to_string(),
|
||||
name: "Base policy".to_string(),
|
||||
},
|
||||
cloud_value,
|
||||
raw_cloud_toml.to_string(),
|
||||
cloud_base_dir,
|
||||
)],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("config layer stack");
|
||||
|
||||
let rendered = render_to_text(&render_debug_config_lines(&stack));
|
||||
assert!(rendered.contains("enterprise-managed (Base policy, cfg_123) (enabled)"));
|
||||
assert!(rendered.contains("Enterprise-managed config value:"));
|
||||
assert!(!rendered.contains("MDM value:"));
|
||||
assert!(rendered.contains("# managed by cloud"));
|
||||
assert!(rendered.contains("model = \"enterprise_model\""));
|
||||
assert!(rendered.contains("approval_policy = \"never\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_config_output_normalizes_empty_web_search_mode_list() {
|
||||
let requirements = ConfigRequirements {
|
||||
|
||||
Reference in New Issue
Block a user