From b495c1094067fea117cb648b19336d5398475418 Mon Sep 17 00:00:00 2001 From: gt-oai Date: Wed, 4 Feb 2026 16:14:46 +0000 Subject: [PATCH] Add /debug-config --- .../src/config_loader/config_requirements.rs | 6 + codex-rs/tui/src/chatwidget.rs | 7 + codex-rs/tui/src/debug_config.rs | 338 ++++++++++++++++++ codex-rs/tui/src/lib.rs | 1 + codex-rs/tui/src/slash_command.rs | 3 + 5 files changed, 355 insertions(+) create mode 100644 codex-rs/tui/src/debug_config.rs diff --git a/codex-rs/core/src/config_loader/config_requirements.rs b/codex-rs/core/src/config_loader/config_requirements.rs index 660628c04f5..b3e60432887 100644 --- a/codex-rs/core/src/config_loader/config_requirements.rs +++ b/codex-rs/core/src/config_loader/config_requirements.rs @@ -99,6 +99,12 @@ impl Default for ConfigRequirements { } } +impl ConfigRequirements { + pub fn exec_policy_source(&self) -> Option<&RequirementSource> { + self.exec_policy.as_ref().map(|policy| &policy.source) + } +} + #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(untagged)] pub enum McpServerIdentity { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 52a1b544044..8167e176ec5 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -3005,6 +3005,9 @@ impl ChatWidget { SlashCommand::Status => { self.add_status_output(); } + SlashCommand::DebugConfig => { + self.add_debug_config_output(); + } SlashCommand::Ps => { self.add_ps_output(); } @@ -3780,6 +3783,10 @@ impl ChatWidget { )); } + pub(crate) fn add_debug_config_output(&mut self) { + self.add_to_history(crate::debug_config::new_debug_config_output(&self.config)); + } + pub(crate) fn add_ps_output(&mut self) { let processes = self .unified_exec_processes diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs new file mode 100644 index 00000000000..4202264fb2e --- /dev/null +++ b/codex-rs/tui/src/debug_config.rs @@ -0,0 +1,338 @@ +use crate::history_cell::PlainHistoryCell; +use codex_app_server_protocol::ConfigLayerSource; +use codex_core::config::Config; +use codex_core::config_loader::ConfigLayerStack; +use codex_core::config_loader::ConfigLayerStackOrdering; +use codex_core::config_loader::RequirementSource; +use codex_core::config_loader::ResidencyRequirement; +use codex_core::config_loader::SandboxModeRequirement; +use ratatui::style::Stylize; +use ratatui::text::Line; + +pub(crate) fn new_debug_config_output(config: &Config) -> PlainHistoryCell { + PlainHistoryCell::new(render_debug_config_lines(&config.config_layer_stack)) +} + +fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec> { + let mut lines = vec!["/debug-config".magenta().into(), "".into()]; + + lines.push( + "Config layer stack (lowest precedence first):" + .bold() + .into(), + ); + let layers = stack.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, true); + if layers.is_empty() { + lines.push(" ".dim().into()); + } else { + for (index, layer) in layers.iter().enumerate() { + let source = format_config_layer_source(&layer.name); + let status = if layer.is_disabled() { + "disabled" + } else { + "enabled" + }; + lines.push(format!(" {}. {source} ({status})", index + 1).into()); + if let Some(reason) = &layer.disabled_reason { + lines.push(format!(" reason: {reason}").dim().into()); + } + } + } + + let requirements = stack.requirements(); + let requirements_toml = stack.requirements_toml(); + + lines.push("".into()); + lines.push("Requirements:".bold().into()); + let mut requirement_lines = Vec::new(); + + if let Some(policies) = requirements_toml.allowed_approval_policies.as_ref() { + let value = join_or_empty(policies.iter().map(ToString::to_string).collect::>()); + requirement_lines.push(requirement_line( + "allowed_approval_policies", + value, + requirements.approval_policy.source.as_ref(), + )); + } + + if let Some(modes) = requirements_toml.allowed_sandbox_modes.as_ref() { + let value = join_or_empty( + modes + .iter() + .copied() + .map(format_sandbox_mode_requirement) + .collect::>(), + ); + requirement_lines.push(requirement_line( + "allowed_sandbox_modes", + value, + requirements.sandbox_policy.source.as_ref(), + )); + } + + if let Some(servers) = requirements_toml.mcp_servers.as_ref() { + let value = join_or_empty(servers.keys().cloned().collect::>()); + requirement_lines.push(requirement_line( + "mcp_servers", + value, + requirements + .mcp_servers + .as_ref() + .map(|sourced| &sourced.source), + )); + } + + // TODO(gt): Expand this debug output with detailed skills and rules display. + if requirements_toml.rules.is_some() { + requirement_lines.push(requirement_line( + "rules", + "configured".to_string(), + requirements.exec_policy_source(), + )); + } + + if let Some(residency) = requirements_toml.enforce_residency { + requirement_lines.push(requirement_line( + "enforce_residency", + format_residency_requirement(residency), + requirements.enforce_residency.source.as_ref(), + )); + } + + if requirement_lines.is_empty() { + lines.push(" ".dim().into()); + } else { + lines.extend(requirement_lines); + } + + lines +} + +fn requirement_line( + name: &str, + value: String, + source: Option<&RequirementSource>, +) -> Line<'static> { + let source = source + .map(ToString::to_string) + .unwrap_or_else(|| "".to_string()); + format!(" - {name}: {value} (source: {source})").into() +} + +fn join_or_empty(values: Vec) -> String { + if values.is_empty() { + "".to_string() + } else { + values.join(", ") + } +} + +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(), + SandboxModeRequirement::WorkspaceWrite => "workspace-write".to_string(), + SandboxModeRequirement::DangerFullAccess => "danger-full-access".to_string(), + SandboxModeRequirement::ExternalSandbox => "external-sandbox".to_string(), + } +} + +fn format_residency_requirement(requirement: ResidencyRequirement) -> String { + match requirement { + ResidencyRequirement::Us => "us".to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::render_debug_config_lines; + use codex_app_server_protocol::ConfigLayerSource; + use codex_core::config::Constrained; + use codex_core::config_loader::ConfigLayerEntry; + use codex_core::config_loader::ConfigLayerStack; + use codex_core::config_loader::ConfigRequirements; + use codex_core::config_loader::ConfigRequirementsToml; + use codex_core::config_loader::ConstrainedWithSource; + use codex_core::config_loader::McpServerIdentity; + use codex_core::config_loader::McpServerRequirement; + use codex_core::config_loader::RequirementSource; + use codex_core::config_loader::ResidencyRequirement; + use codex_core::config_loader::SandboxModeRequirement; + use codex_core::config_loader::Sourced; + use codex_core::protocol::AskForApproval; + use codex_core::protocol::SandboxPolicy; + use codex_utils_absolute_path::AbsolutePathBuf; + use ratatui::text::Line; + use std::collections::BTreeMap; + use toml::Value as TomlValue; + + fn empty_toml_table() -> TomlValue { + TomlValue::Table(toml::map::Map::new()) + } + + fn absolute_path(path: &str) -> AbsolutePathBuf { + AbsolutePathBuf::from_absolute_path(path).expect("absolute path") + } + + fn render_to_text(lines: &[Line<'static>]) -> String { + lines + .iter() + .map(|line| { + line.spans + .iter() + .map(|span| span.content.as_ref()) + .collect::() + }) + .collect::>() + .join("\n") + } + + #[test] + fn debug_config_output_lists_all_layers_including_disabled() { + let system_file = if cfg!(windows) { + absolute_path("C:\\etc\\codex\\config.toml") + } else { + absolute_path("/etc/codex/config.toml") + }; + let project_folder = if cfg!(windows) { + absolute_path("C:\\repo\\.codex") + } else { + absolute_path("/repo/.codex") + }; + + let layers = vec![ + ConfigLayerEntry::new( + ConfigLayerSource::System { file: system_file }, + empty_toml_table(), + ), + ConfigLayerEntry::new_disabled( + ConfigLayerSource::Project { + dot_codex_folder: project_folder, + }, + empty_toml_table(), + "project is untrusted", + ), + ]; + let stack = ConfigLayerStack::new( + layers, + ConfigRequirements::default(), + ConfigRequirementsToml::default(), + ) + .expect("config layer stack"); + + let rendered = render_to_text(&render_debug_config_lines(&stack)); + assert!(rendered.contains("(enabled)")); + assert!(rendered.contains("(disabled)")); + assert!(rendered.contains("reason: project is untrusted")); + assert!(rendered.contains("Requirements:")); + assert!(rendered.contains(" ")); + } + + #[test] + fn debug_config_output_lists_requirement_sources() { + let requirements_file = if cfg!(windows) { + absolute_path("C:\\etc\\codex\\requirements.toml") + } else { + absolute_path("/etc/codex/requirements.toml") + }; + let mut requirements = ConfigRequirements::default(); + requirements.approval_policy = ConstrainedWithSource::new( + Constrained::allow_any(AskForApproval::OnRequest), + Some(RequirementSource::CloudRequirements), + ); + requirements.sandbox_policy = ConstrainedWithSource::new( + Constrained::allow_any(SandboxPolicy::ReadOnly), + Some(RequirementSource::SystemRequirementsToml { + file: requirements_file.clone(), + }), + ); + requirements.mcp_servers = Some(Sourced::new( + BTreeMap::from([( + "docs".to_string(), + McpServerRequirement { + identity: McpServerIdentity::Command { + command: "codex-mcp".to_string(), + }, + }, + )]), + RequirementSource::LegacyManagedConfigTomlFromMdm, + )); + requirements.enforce_residency = ConstrainedWithSource::new( + Constrained::allow_any(Some(ResidencyRequirement::Us)), + Some(RequirementSource::CloudRequirements), + ); + + let requirements_toml = ConfigRequirementsToml { + allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), + allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), + mcp_servers: Some(BTreeMap::from([( + "docs".to_string(), + McpServerRequirement { + identity: McpServerIdentity::Command { + command: "codex-mcp".to_string(), + }, + }, + )])), + rules: None, + enforce_residency: Some(ResidencyRequirement::Us), + }; + + let user_file = if cfg!(windows) { + absolute_path("C:\\users\\alice\\.codex\\config.toml") + } else { + absolute_path("/home/alice/.codex/config.toml") + }; + let stack = ConfigLayerStack::new( + vec![ConfigLayerEntry::new( + ConfigLayerSource::User { file: user_file }, + empty_toml_table(), + )], + requirements, + requirements_toml, + ) + .expect("config layer stack"); + + let rendered = render_to_text(&render_debug_config_lines(&stack)); + assert!( + rendered.contains("allowed_approval_policies: on-request (source: cloud requirements)") + ); + assert!( + rendered.contains( + format!( + "allowed_sandbox_modes: read-only (source: {})", + requirements_file.as_path().display() + ) + .as_str(), + ) + ); + assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))")); + assert!(rendered.contains("enforce_residency: us (source: cloud requirements)")); + assert!(!rendered.contains(" - rules:")); + } +} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 2e5aee5c382..2dbc1fcaf98 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -67,6 +67,7 @@ mod collaboration_modes; mod color; pub mod custom_terminal; mod cwd_prompt; +mod debug_config; mod diff_render; mod exec_cell; mod exec_command; diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index a71b1ddbe54..e4ac5dcacf4 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -33,6 +33,7 @@ pub enum SlashCommand { Diff, Mention, Status, + DebugConfig, Mcp, Apps, Logout, @@ -63,6 +64,7 @@ impl SlashCommand { SlashCommand::Mention => "mention a file", SlashCommand::Skills => "use skills to improve how Codex performs specific tasks", SlashCommand::Status => "show current session configuration and token usage", + SlashCommand::DebugConfig => "show config layers and requirement sources for debugging", SlashCommand::Ps => "list background terminals", SlashCommand::Model => "choose what model and reasoning effort to use", SlashCommand::Personality => "choose a communication style for Codex", @@ -118,6 +120,7 @@ impl SlashCommand { | SlashCommand::Mention | SlashCommand::Skills | SlashCommand::Status + | SlashCommand::DebugConfig | SlashCommand::Ps | SlashCommand::Mcp | SlashCommand::Apps