tui: show non-file layer content in /debug-config (#11412)

The debug output listed non-file-backed layers such as session flags and
MDM managed config, but it did not show their values. That made it
difficult to explain unexpected effective settings because users could
not inspect those layers on disk.

Now `/debug-config` might include output like this:

```
Config layer stack (lowest precedence first):
  1. system (/etc/codex/config.toml) (enabled)
  2. user (/Users/mbolin/.codex/config.toml) (enabled)
  3. legacy managed_config.toml (mdm) (enabled)
     MDM value:
       # Production Codex configuration file.

       [otel]
       log_user_prompt = true
       environment = "prod"
       exporter = { otlp-http = {
         endpoint = "https://example.com/otel",
         protocol = "binary"
       }}
```
This commit is contained in:
Michael Bolin 2026-02-10 22:23:08 -08:00 committed by GitHub
parent fdd0cd1de9
commit f6dd9e37e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 223 additions and 20 deletions

View file

@ -2,6 +2,8 @@ use super::LoaderOverrides;
use super::diagnostics::config_error_from_toml;
use super::diagnostics::io_error_from_config_error;
#[cfg(target_os = "macos")]
use super::macos::ManagedAdminConfigLayer;
#[cfg(target_os = "macos")]
use super::macos::load_managed_admin_config_layer;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::io;
@ -19,12 +21,18 @@ pub(super) struct MangedConfigFromFile {
pub file: AbsolutePathBuf,
}
#[derive(Debug, Clone)]
pub(super) struct ManagedConfigFromMdm {
pub managed_config: TomlValue,
pub raw_toml: String,
}
#[derive(Debug, Clone)]
pub(super) struct LoadedConfigLayers {
/// If present, data read from a file such as `/etc/codex/managed_config.toml`.
pub managed_config: Option<MangedConfigFromFile>,
/// If present, data read from managed preferences (macOS only).
pub managed_config_from_mdm: Option<TomlValue>,
pub managed_config_from_mdm: Option<ManagedConfigFromMdm>,
}
pub(super) async fn load_config_layers_internal(
@ -57,7 +65,9 @@ pub(super) async fn load_config_layers_internal(
#[cfg(target_os = "macos")]
let managed_preferences =
load_managed_admin_config_layer(managed_preferences_base64.as_deref()).await?;
load_managed_admin_config_layer(managed_preferences_base64.as_deref())
.await?
.map(map_managed_admin_layer);
#[cfg(not(target_os = "macos"))]
let managed_preferences = None;
@ -68,6 +78,15 @@ pub(super) async fn load_config_layers_internal(
})
}
#[cfg(target_os = "macos")]
fn map_managed_admin_layer(layer: ManagedAdminConfigLayer) -> ManagedConfigFromMdm {
let ManagedAdminConfigLayer { config, raw_toml } = layer;
ManagedConfigFromMdm {
managed_config: config,
raw_toml,
}
}
pub(super) async fn read_config_from_path(
path: impl AsRef<Path>,
log_missing_as_info: bool,

View file

@ -15,6 +15,12 @@ const MANAGED_PREFERENCES_APPLICATION_ID: &str = "com.openai.codex";
const MANAGED_PREFERENCES_CONFIG_KEY: &str = "config_toml_base64";
const MANAGED_PREFERENCES_REQUIREMENTS_KEY: &str = "requirements_toml_base64";
#[derive(Debug, Clone)]
pub(super) struct ManagedAdminConfigLayer {
pub config: TomlValue,
pub raw_toml: String,
}
pub(super) fn managed_preferences_requirements_source() -> RequirementSource {
RequirementSource::MdmManagedPreferences {
domain: MANAGED_PREFERENCES_APPLICATION_ID.to_string(),
@ -24,7 +30,7 @@ pub(super) fn managed_preferences_requirements_source() -> RequirementSource {
pub(crate) async fn load_managed_admin_config_layer(
override_base64: Option<&str>,
) -> io::Result<Option<TomlValue>> {
) -> io::Result<Option<ManagedAdminConfigLayer>> {
if let Some(encoded) = override_base64 {
let trimmed = encoded.trim();
return if trimmed.is_empty() {
@ -47,7 +53,7 @@ pub(crate) async fn load_managed_admin_config_layer(
}
}
fn load_managed_admin_config() -> io::Result<Option<TomlValue>> {
fn load_managed_admin_config() -> io::Result<Option<ManagedAdminConfigLayer>> {
load_managed_preference(MANAGED_PREFERENCES_CONFIG_KEY)?
.as_deref()
.map(str::trim)
@ -122,9 +128,13 @@ fn load_managed_preference(key_name: &str) -> io::Result<Option<String>> {
Ok(Some(value))
}
fn parse_managed_config_base64(encoded: &str) -> io::Result<TomlValue> {
match toml::from_str::<TomlValue>(&decode_managed_preferences_base64(encoded)?) {
Ok(TomlValue::Table(parsed)) => Ok(TomlValue::Table(parsed)),
fn parse_managed_config_base64(encoded: &str) -> io::Result<ManagedAdminConfigLayer> {
let raw_toml = decode_managed_preferences_base64(encoded)?;
match toml::from_str::<TomlValue>(&raw_toml) {
Ok(TomlValue::Table(parsed)) => Ok(ManagedAdminConfigLayer {
config: TomlValue::Table(parsed),
raw_toml,
}),
Ok(other) => {
tracing::error!("Managed config TOML must have a table at the root, found {other:?}",);
Err(io::Error::new(

View file

@ -278,9 +278,10 @@ pub async fn load_config_layers_state(
));
}
if let Some(config) = managed_config_from_mdm {
layers.push(ConfigLayerEntry::new(
layers.push(ConfigLayerEntry::new_with_raw_toml(
ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
config,
config.managed_config,
config.raw_toml,
));
}
@ -485,7 +486,12 @@ async fn load_requirements_from_legacy_scheme(
} = loaded_config_layers;
for (source, config) in managed_config_from_mdm
.map(|config| (RequirementSource::LegacyManagedConfigTomlFromMdm, config))
.map(|config| {
(
RequirementSource::LegacyManagedConfigTomlFromMdm,
config.managed_config,
)
})
.into_iter()
.chain(managed_config.map(|c| {
(

View file

@ -27,6 +27,7 @@ pub struct LoaderOverrides {
pub struct ConfigLayerEntry {
pub name: ConfigLayerSource,
pub config: TomlValue,
pub raw_toml: Option<String>,
pub version: String,
pub disabled_reason: Option<String>,
}
@ -37,6 +38,18 @@ impl ConfigLayerEntry {
Self {
name,
config,
raw_toml: None,
version,
disabled_reason: None,
}
}
pub fn new_with_raw_toml(name: ConfigLayerSource, config: TomlValue, raw_toml: String) -> Self {
let version = version_for_toml(&config);
Self {
name,
config,
raw_toml: Some(raw_toml),
version,
disabled_reason: None,
}
@ -51,6 +64,7 @@ impl ConfigLayerEntry {
Self {
name,
config,
raw_toml: None,
version,
disabled_reason: Some(disabled_reason.into()),
}
@ -60,6 +74,10 @@ impl ConfigLayerEntry {
self.disabled_reason.is_some()
}
pub fn raw_toml(&self) -> Option<&str> {
self.raw_toml.as_deref()
}
pub fn metadata(&self) -> ConfigLayerMetadata {
ConfigLayerMetadata {
name: self.name.clone(),

View file

@ -264,6 +264,7 @@ async fn returns_empty_when_all_layers_missing() {
.expect("resolve user config.toml path")
},
config: TomlValue::Table(toml::map::Map::new()),
raw_toml: None,
version: version_for_toml(&TomlValue::Table(toml::map::Map::new())),
disabled_reason: None,
},
@ -325,18 +326,17 @@ flag = true
"#,
)
.expect("write managed config");
let raw_managed_preferences = r#"
# managed profile
[nested]
value = "managed"
flag = false
"#;
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path),
managed_preferences_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
[nested]
value = "managed"
flag = false
"#
.as_bytes(),
),
base64::prelude::BASE64_STANDARD.encode(raw_managed_preferences.as_bytes()),
),
macos_managed_config_requirements_base64: None,
};
@ -361,6 +361,19 @@ flag = false
Some(&TomlValue::String("managed".to_string()))
);
assert_eq!(nested.get("flag"), Some(&TomlValue::Boolean(false)));
let mdm_layer = state
.layers_high_to_low()
.into_iter()
.find(|layer| {
matches!(
layer.name,
super::ConfigLayerSource::LegacyManagedConfigTomlFromMdm
)
})
.expect("mdm layer");
let raw = mdm_layer.raw_toml().expect("preserved mdm toml");
assert!(raw.contains("# managed profile"));
assert!(raw.contains("value = \"managed\""));
}
#[cfg(target_os = "macos")]
@ -862,6 +875,7 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s
dot_codex_folder: AbsolutePathBuf::from_absolute_path(project_root.join(".codex"))?,
},
config: TomlValue::Table(toml::map::Map::new()),
raw_toml: None,
version: version_for_toml(&TomlValue::Table(toml::map::Map::new())),
disabled_reason: None,
}],
@ -955,6 +969,7 @@ async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Resul
dot_codex_folder: AbsolutePathBuf::from_absolute_path(&nested_dot_codex)?,
},
config: child_config.clone(),
raw_toml: None,
version: version_for_toml(&child_config),
disabled_reason: None,
}],

View file

@ -1,6 +1,7 @@
use crate::history_cell::PlainHistoryCell;
use codex_app_server_protocol::ConfigLayerSource;
use codex_core::config::Config;
use codex_core::config_loader::ConfigLayerEntry;
use codex_core::config_loader::ConfigLayerStack;
use codex_core::config_loader::ConfigLayerStackOrdering;
use codex_core::config_loader::NetworkConstraints;
@ -11,6 +12,7 @@ use codex_core::config_loader::WebSearchModeRequirement;
use codex_core::protocol::SessionNetworkProxyRuntime;
use ratatui::style::Stylize;
use ratatui::text::Line;
use toml::Value as TomlValue;
pub(crate) fn new_debug_config_output(
config: &Config,
@ -71,6 +73,7 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
"enabled"
};
lines.push(format!(" {}. {source} ({status})", index + 1).into());
lines.extend(render_non_file_layer_details(layer));
if let Some(reason) = &layer.disabled_reason {
lines.push(format!(" reason: {reason}").dim().into());
}
@ -169,6 +172,80 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
lines
}
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::System { .. }
| ConfigLayerSource::User { .. }
| ConfigLayerSource::Project { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => Vec::new(),
}
}
fn render_session_flag_details(config: &TomlValue) -> Vec<Line<'static>> {
let mut pairs = Vec::new();
flatten_toml_key_values(config, None, &mut pairs);
if pairs.is_empty() {
return vec![" - <none>".dim().into()];
}
pairs
.into_iter()
.map(|(key, value)| format!(" - {key} = {value}").into())
.collect()
}
fn render_mdm_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>> {
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()];
}
if value.contains('\n') {
let mut lines = vec![" MDM value:".into()];
lines.extend(value.lines().map(|line| format!(" {line}").into()));
lines
} else {
vec![format!(" MDM value: {value}").into()]
}
}
fn flatten_toml_key_values(
value: &TomlValue,
prefix: Option<&str>,
out: &mut Vec<(String, String)>,
) {
match value {
TomlValue::Table(table) => {
let mut entries = table.iter().collect::<Vec<_>>();
entries.sort_by_key(|(key, _)| key.as_str());
for (key, child) in entries {
let next_prefix = if let Some(prefix) = prefix {
format!("{prefix}.{key}")
} else {
key.to_string()
};
flatten_toml_key_values(child, Some(&next_prefix), out);
}
}
_ => {
let key = prefix.unwrap_or("<value>").to_string();
out.push((key, format_toml_value(value)));
}
}
}
fn format_toml_value(value: &TomlValue) -> String {
value.to_string()
}
fn requirement_line(
name: &str,
value: String,
@ -205,7 +282,7 @@ fn normalize_allowed_web_search_modes(
fn format_config_layer_source(source: &ConfigLayerSource) -> String {
match source {
ConfigLayerSource::Mdm { domain, key } => {
format!("mdm ({domain}:{key})")
format!("MDM ({domain}:{key})")
}
ConfigLayerSource::System { file } => {
format!("system ({})", file.as_path().display())
@ -224,7 +301,7 @@ fn format_config_layer_source(source: &ConfigLayerSource) -> String {
format!("legacy managed_config.toml ({})", file.as_path().display())
}
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
"legacy managed_config.toml (mdm)".to_string()
"legacy managed_config.toml (MDM)".to_string()
}
}
}
@ -494,6 +571,64 @@ mod tests {
assert!(!rendered.contains(" - rules:"));
}
#[test]
fn debug_config_output_lists_session_flag_key_value_pairs() {
let session_flags = toml::from_str::<TomlValue>(
r#"
model = "gpt-5"
[sandbox_workspace_write]
network_access = true
writable_roots = ["/tmp"]
"#,
)
.expect("session flags");
let stack = ConfigLayerStack::new(
vec![ConfigLayerEntry::new(
ConfigLayerSource::SessionFlags,
session_flags,
)],
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
assert!(rendered.contains("session-flags (enabled)"));
assert!(rendered.contains(" - model = \"gpt-5\""));
assert!(rendered.contains(" - sandbox_workspace_write.network_access = true"));
assert!(rendered.contains("sandbox_workspace_write.writable_roots"));
assert!(rendered.contains("/tmp"));
}
#[test]
fn debug_config_output_shows_legacy_mdm_layer_value() {
let raw_mdm_toml = r#"
# managed by MDM
model = "managed_model"
approval_policy = "never"
"#;
let mdm_value = toml::from_str::<TomlValue>(raw_mdm_toml).expect("MDM value");
let stack = ConfigLayerStack::new(
vec![ConfigLayerEntry::new_with_raw_toml(
ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
mdm_value,
raw_mdm_toml.to_string(),
)],
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
assert!(rendered.contains("legacy managed_config.toml (MDM) (enabled)"));
assert!(rendered.contains("MDM value:"));
assert!(rendered.contains("# managed by MDM"));
assert!(rendered.contains("model = \"managed_model\""));
assert!(rendered.contains("approval_policy = \"never\""));
}
#[test]
fn debug_config_output_normalizes_empty_web_search_mode_list() {
let mut requirements = ConfigRequirements::default();