Add cloud managed config layer support

Introduce an explicit enterprise-managed config layer source and the client-side machinery to materialize cloud-delivered config TOML fragments into the normal config stack. The new ConfigLayerSource::EnterpriseManaged variant carries the backend layer id and display name so diagnostics and debug output can point admins at the exact cloud layer that needs fixing.

Add codex_config::cloud_config_layers to build config layers from delivered fragments. The composition keeps backend layer order deterministic, resolves relative path settings against a supplied base directory for consistency with existing MDM-delivered config semantics, and stores the raw TOML with that base directory on ConfigLayerEntry so typed diagnostics can reparse non-file layers without relying on a synthetic filesystem path.

Keep this v1 pull-based and snapshot-oriented. The bundle loader/cache work can feed these helpers, but this change does not introduce dynamic refresh or announce/push semantics. Consumers continue to read the config state they are already handed.

Tighten provenance and diagnostics for non-file layers: enterprise-managed layers render as enterprise-managed config values in debug output, syntax/type errors use the layer display name, and synthetic hook source paths include the enterprise layer name/id when a filesystem path is needed for existing hook metadata surfaces.

Split hook provenance semantically by adding HookSource::CloudManagedConfig. Hooks delivered through enterprise-managed config layers now report cloud_managed_config / cloudManagedConfig, while hooks delivered through requirements remain CloudRequirements. The TUI labels the new source as Cloud-managed config, and analytics/core metric mappings were updated to include the new source.

Regenerate app-server protocol JSON and TypeScript schema fixtures for the new ConfigLayerSource and HookSource wire values.

Verification: just write-app-server-schema; cargo test -p codex-app-server-protocol; cargo test -p codex-hooks hook_metadata_for_config_layer_source; cargo test -p codex-core hook_run_metric_tags; cargo test -p codex-analytics hook_run_metadata; just fmt; just fix -p codex-protocol -p codex-app-server-protocol -p codex-hooks -p codex-analytics -p codex-core -p codex-tui.
This commit is contained in:
Joe Florencio
2026-05-20 14:17:57 -07:00
parent fac7c71098
commit 9f75a0d395
25 changed files with 624 additions and 20 deletions

View File

@@ -1005,6 +1005,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",

View File

@@ -2002,6 +2002,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -7557,6 +7557,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": {
@@ -10036,6 +10063,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -3926,6 +3926,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": {
@@ -6516,6 +6543,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -166,6 +166,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -166,6 +166,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -130,6 +130,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -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.

View File

@@ -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";

View File

@@ -42,6 +42,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
@@ -89,6 +102,7 @@ impl ConfigLayerSource {
match self {
ConfigLayerSource::Mdm { .. } => 0,
ConfigLayerSource::System { .. } => 10,
ConfigLayerSource::EnterpriseManaged { .. } => 15,
ConfigLayerSource::User { profile, .. } => {
if profile.is_some() {
21

View File

@@ -48,6 +48,7 @@ v2_enum_from_core!(
SessionFlags,
Plugin,
CloudRequirements,
CloudManagedConfig,
LegacyManagedConfigFile,
LegacyManagedConfigMdm,
Unknown,

View File

@@ -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(),

View File

@@ -0,0 +1,312 @@
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)]
mod tests {
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 config 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`"));
}
}

View File

@@ -89,7 +89,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 +96,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 +199,27 @@ where
I: IntoIterator<Item = &'a ConfigLayerEntry>,
{
for layer in layers {
if let Some(contents) = layer.raw_toml() {
let source_name = display_name_for_layer(layer, 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,11 +254,32 @@ 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,
}
}
fn display_name_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> String {
match &layer.name {
ConfigLayerSource::EnterpriseManaged { id, name } => {
format!("enterprise-managed config {name} ({id})")
}
ConfigLayerSource::Mdm { domain, key } => format!("managed policy {domain}:{key}"),
ConfigLayerSource::SessionFlags => "session flags".to_string(),
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
"legacy managed configuration from MDM".to_string()
}
ConfigLayerSource::System { file }
| ConfigLayerSource::User { file, .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => file.display().to_string(),
ConfigLayerSource::Project { dot_codex_folder } => dot_codex_folder
.join(config_toml_file)
.display()
.to_string(),
}
}
pub(crate) fn text_range_from_span(contents: &str, span: std::ops::Range<usize>) -> TextRange {
let start = position_for_offset(contents, span.start);
let end_index = if span.end > span.start {

View File

@@ -1,3 +1,4 @@
mod cloud_config_layers;
mod cloud_requirements;
mod cloud_requirements_composition;
mod config_requirements;
@@ -29,6 +30,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;

View File

@@ -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,
));
}

View File

@@ -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,

View File

@@ -339,6 +339,7 @@ fn skill_roots_from_layer_stack_inner(
});
}
ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::EnterpriseManaged { .. }
| ConfigLayerSource::SessionFlags
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {}

View File

@@ -701,6 +701,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",

View File

@@ -364,6 +364,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")
}
@@ -589,6 +592,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)
@@ -994,6 +998,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),

View File

@@ -1374,6 +1374,7 @@ pub enum HookSource {
SessionFlags,
Plugin,
CloudRequirements,
CloudManagedConfig,
LegacyManagedConfigFile,
LegacyManagedConfigMdm,
#[default]

View File

@@ -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",

View File

@@ -267,9 +267,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 +308,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",
}
}
@@ -396,6 +411,9 @@ fn format_config_layer_source(source: &ConfigLayerSource) -> String {
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())
}
@@ -896,12 +914,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(),
@@ -916,6 +940,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 {