Add /debug-config slash command (#10642)

<img width="409" height="175" alt="image"
src="https://github.com/user-attachments/assets/76efe9c5-8375-4af3-b6af-bd9e162c1bc3"
/>
This commit is contained in:
gt-oai
2026-02-04 22:26:17 +00:00
committed by GitHub
parent 7c6d21a414
commit d452bb3ae5
5 changed files with 355 additions and 0 deletions

View File

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

View File

@@ -3006,6 +3006,9 @@ impl ChatWidget {
SlashCommand::Status => {
self.add_status_output();
}
SlashCommand::DebugConfig => {
self.add_debug_config_output();
}
SlashCommand::Ps => {
self.add_ps_output();
}
@@ -3781,6 +3784,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

View File

@@ -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<Line<'static>> {
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(" <none>".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::<Vec<_>>());
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::<Vec<_>>(),
);
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::<Vec<_>>());
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(" <none>".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(|| "<unspecified>".to_string());
format!(" - {name}: {value} (source: {source})").into()
}
fn join_or_empty(values: Vec<String>) -> String {
if values.is_empty() {
"<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::<String>()
})
.collect::<Vec<_>>()
.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(" <none>"));
}
#[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:"));
}
}

View File

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

View File

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