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:
parent
fdd0cd1de9
commit
f6dd9e37e7
6 changed files with 223 additions and 20 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue