Files
codex/codex-rs/tui/src/debug_config.rs
T
Eric Traut 2df465732a Use app-server MCP configuration and startup events in the TUI (#60622)
## Why

Locally configured MCP servers may differ from those reported by the app server. Waiting for unreported servers can leave startup progress busy after the observed servers have finished.

## What changed

- Track startup progress from received server status updates, settling when no observed starts remain pending. Handle standalone outcomes after attachment or lag, deduplicate repeated diagnostics, and reactivate progress on new starts.
- Read effective MCP configuration through `config/read` to prevent task tools from shadowing the `codex_tui` namespace, including when configuration changes after startup.
- Use the same configuration reader to disable MCP servers for temporary structured threads.
- Remove client-side MCP requirement checks for the task-tools server and MCP requirements from debug configuration output, leaving enforcement to the app server.

## Testing

Update regression coverage for unreported local servers, startup rounds after lag, diagnostic handling, namespace conflicts, and server-enforced managed requirements.

GitOrigin-RevId: 370767b90ee55f8b0b1403872ef437d53257586b
2026-11-10 16:44:23 +00:00

1458 lines
52 KiB
Rust

use crate::history_cell::PlainHistoryCell;
use crate::legacy_core::config::Config;
use crate::legacy_core::config::Permissions;
use crate::session_state::SessionNetworkProxyRuntime;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerSource;
use codex_config::ConfigLayerStack;
use codex_config::ManagedHooksRequirementsToml;
use codex_config::NetworkConstraints;
use codex_config::NetworkDomainPermissionToml;
use codex_config::NetworkUnixSocketPermissionToml;
use codex_config::RequirementSource;
use codex_config::ResidencyRequirement;
use codex_config::SandboxModeRequirement;
use codex_config::WebSearchModeRequirement;
use codex_config::format_config_layer_source;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use ratatui::style::Stylize;
use ratatui::text::Line;
use toml::Value as TomlValue;
pub(crate) fn new_debug_config_output(
config: &Config,
session_network_proxy: Option<&SessionNetworkProxyRuntime>,
) -> PlainHistoryCell {
let mut lines = render_debug_config_lines(&config.config_layer_stack, |mode| {
sandbox_mode_is_allowed_by_permissions(&config.permissions, mode)
});
lines.extend(render_agents_config_lines(config));
if let Some(proxy) = session_network_proxy {
lines.push("".into());
lines.push("Session runtime:".bold().into());
lines.push(" - network_proxy".into());
let SessionNetworkProxyRuntime {
http_addr,
socks_addr,
} = proxy;
let all_proxy = session_all_proxy_url(
http_addr,
socks_addr,
config
.permissions
.network
.as_ref()
.is_some_and(crate::legacy_core::config::NetworkProxySpec::socks_enabled),
);
lines.push(format!(" - HTTP_PROXY = http://{http_addr}").into());
lines.push(format!(" - ALL_PROXY = {all_proxy}").into());
}
PlainHistoryCell::new(lines)
}
fn render_agents_config_lines(config: &Config) -> Vec<Line<'static>> {
vec![
"".into(),
"[agents]:".bold().into(),
format!(" - enabled = {}", config.agents_enabled).into(),
format!(
" - max_concurrent_threads_per_session = {}",
format_optional(config.agent_max_threads)
)
.into(),
format!(
" - max_depth = {} (V1 only; ignored by V2)",
config.agent_max_depth
)
.into(),
format!(
" - default_subagent_model = {}",
format_optional(config.agent_default_subagent_model.as_deref())
)
.into(),
format!(
" - default_subagent_reasoning_effort = {}",
format_optional(config.agent_default_subagent_reasoning_effort.as_ref())
)
.into(),
format!(
" - interrupt_message = {}",
config.agent_interrupt_message_enabled
)
.into(),
]
}
fn format_optional(value: Option<impl std::fmt::Display>) -> String {
value
.map(|value| value.to_string())
.unwrap_or_else(|| "<unset>".to_string())
}
fn sandbox_mode_is_allowed_by_permissions(
permissions: &Permissions,
mode: SandboxModeRequirement,
) -> bool {
let permission_profile = match mode {
SandboxModeRequirement::ReadOnly => PermissionProfile::read_only(),
SandboxModeRequirement::WorkspaceWrite => PermissionProfile::workspace_write(),
SandboxModeRequirement::DangerFullAccess => PermissionProfile::Disabled,
SandboxModeRequirement::ExternalSandbox => PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
},
};
permissions
.can_set_permission_profile(&permission_profile)
.is_ok()
}
fn session_all_proxy_url(http_addr: &str, socks_addr: &str, socks_enabled: bool) -> String {
if socks_enabled {
format!("socks5h://{socks_addr}")
} else {
format!("http://{http_addr}")
}
}
fn render_debug_config_lines(
stack: &ConfigLayerStack,
sandbox_mode_is_effectively_allowed: impl Fn(SandboxModeRequirement) -> bool,
) -> Vec<Line<'static>> {
let mut lines = vec!["/debug-config".magenta().into(), "".into()];
lines.push(
"Config layer stack (lowest precedence first):"
.bold()
.into(),
);
let mut layers = stack.all_layers_low_to_high().peekable();
if layers.peek().is_none() {
lines.push(" <none>".dim().into());
} else {
for (index, layer) in layers.enumerate() {
let source = format_config_layer_source(&layer.name, CONFIG_TOML_FILE);
let status = if layer.is_disabled() {
"disabled"
} else {
"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());
}
}
}
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(sqlite_home) = requirements.sqlite_home.as_ref() {
requirement_lines.push(requirement_line(
"sqlite_home",
sqlite_home.value.display().to_string(),
Some(&sqlite_home.source),
));
}
if let Some(log_dir) = requirements.log_dir.as_ref() {
requirement_lines.push(requirement_line(
"log_dir",
log_dir.value.display().to_string(),
Some(&log_dir.source),
));
}
if let Some(model_catalog_json) = requirements.model_catalog_json.as_ref() {
requirement_lines.push(requirement_line(
"model_catalog_json",
model_catalog_json.value.display().to_string(),
Some(&model_catalog_json.source),
));
}
if let Some(check_for_update_on_startup) = requirements.check_for_update_on_startup.as_ref() {
requirement_lines.push(requirement_line(
"check_for_update_on_startup",
check_for_update_on_startup.value.to_string(),
Some(&check_for_update_on_startup.source),
));
}
if let Some(allow_login_shell) = requirements.allow_login_shell.as_ref() {
requirement_lines.push(requirement_line(
"allow_login_shell",
allow_login_shell.value.to_string(),
Some(&allow_login_shell.source),
));
}
if let Some(feedback) = requirements.feedback.as_ref()
&& let Some(enabled) = feedback.value.enabled
{
requirement_lines.push(requirement_line(
"feedback.enabled",
enabled.to_string(),
Some(&feedback.source),
));
}
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(reviewers) = requirements_toml.allowed_approvals_reviewers.as_ref() {
let value = join_or_empty(
reviewers
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
);
requirement_lines.push(requirement_line(
"allowed_approvals_reviewers",
value,
requirements.approvals_reviewer.source.as_ref(),
));
}
if let Some(modes) = requirements_toml.allowed_sandbox_modes.as_ref() {
let value = join_or_empty(
modes
.iter()
.copied()
.filter(|mode| sandbox_mode_is_effectively_allowed(*mode))
.map(format_sandbox_mode_requirement)
.collect::<Vec<_>>(),
);
requirement_lines.push(requirement_line(
"allowed_sandbox_modes",
value,
requirements.permission_profile.source.as_ref(),
));
}
if let Some(modes) = requirements_toml.allowed_web_search_modes.as_ref() {
let normalized = normalize_allowed_web_search_modes(modes);
let value = join_or_empty(
normalized
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
);
requirement_lines.push(requirement_line(
"allowed_web_search_modes",
value,
requirements.web_search_mode.source.as_ref(),
));
}
if let Some(allow_managed_hooks_only) = requirements_toml.allow_managed_hooks_only {
requirement_lines.push(requirement_line(
"allow_managed_hooks_only",
allow_managed_hooks_only.to_string(),
requirements
.allow_managed_hooks_only
.as_ref()
.map(|sourced| &sourced.source),
));
}
if let Some(allow_appshots) = requirements_toml.allow_appshots {
requirement_lines.push(requirement_line(
"allow_appshots",
allow_appshots.to_string(),
requirements
.allow_appshots
.as_ref()
.map(|sourced| &sourced.source),
));
}
if let Some(allow_remote_control) = requirements_toml.allow_remote_control {
requirement_lines.push(requirement_line(
"allow_remote_control",
allow_remote_control.to_string(),
requirements
.allow_remote_control
.as_ref()
.map(|sourced| &sourced.source),
));
}
if requirements_toml.guardian_policy_config.is_some() {
requirement_lines.push(requirement_line(
"guardian_policy_config",
"configured".to_string(),
requirements.guardian_policy_config_source.as_ref(),
));
}
if let Some(feature_requirements) = requirements.feature_requirements.as_ref() {
let value = join_or_empty(
feature_requirements
.value
.entries
.iter()
.map(|(feature, enabled)| format!("{feature}={enabled}"))
.collect::<Vec<_>>(),
);
requirement_lines.push(requirement_line(
"features",
value,
Some(&feature_requirements.source),
));
}
if let Some(hooks) = requirements_toml.hooks.as_ref() {
requirement_lines.push(requirement_line(
"hooks",
format_managed_hooks_requirements(hooks),
requirements
.managed_hooks
.as_ref()
.and_then(|managed_hooks| managed_hooks.source.as_ref()),
));
}
// 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 let Some(network) = requirements.network.as_ref() {
let key = if requirements_toml
.permissions
.as_ref()
.and_then(|permissions| permissions.network.as_ref())
.is_some()
{
"permissions.network"
} else {
"network_proxy"
};
requirement_lines.push(requirement_line(
key,
format_network_constraints(&network.value),
Some(&network.source),
));
}
if let Some(filesystem) = requirements.filesystem.as_ref() {
let deny_read = join_or_empty(
filesystem
.value
.deny_read
.iter()
.map(|pattern| pattern.as_str().to_string())
.collect::<Vec<_>>(),
);
requirement_lines.push(requirement_line(
"permissions.filesystem.deny_read",
deny_read,
Some(&filesystem.source),
));
}
if requirement_lines.is_empty() {
lines.push(" <none>".dim().into());
} else {
lines.extend(requirement_lines);
}
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::EnterpriseManaged { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => render_non_file_layer_value(layer),
ConfigLayerSource::PackagedDefaults { .. }
| 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, /*prefix*/ None, &mut pairs);
if pairs.is_empty() {
return vec![" - <none>".dim().into()];
}
pairs
.into_iter()
.map(|(key, value)| format!(" - {key} = {value}").into())
.collect()
}
fn format_managed_hooks_requirements(hooks: &ManagedHooksRequirementsToml) -> String {
let mut parts = Vec::new();
if let Some(managed_dir) = hooks.managed_dir.as_ref() {
parts.push(format!("managed_dir={}", managed_dir.display()));
}
if let Some(windows_managed_dir) = hooks.windows_managed_dir.as_ref() {
parts.push(format!(
"windows_managed_dir={}",
windows_managed_dir.display()
));
}
parts.push(format!("handlers={}", hooks.handler_count()));
join_or_empty(parts)
}
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![format!(" {label}: <empty>").dim().into()];
}
if value.contains('\n') {
let mut lines = vec![format!(" {label}:").into()];
lines.extend(value.lines().map(|line| format!(" {line}").into()));
lines
} else {
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::PackagedDefaults { .. }
| ConfigLayerSource::SessionFlags
| ConfigLayerSource::System { .. }
| ConfigLayerSource::User { .. }
| ConfigLayerSource::Project { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => "Layer value",
}
}
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,
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 normalize_allowed_web_search_modes(
modes: &[WebSearchModeRequirement],
) -> Vec<WebSearchModeRequirement> {
if modes.is_empty() {
return vec![WebSearchModeRequirement::Disabled];
}
let mut normalized = modes.to_vec();
if !normalized.contains(&WebSearchModeRequirement::Disabled) {
normalized.push(WebSearchModeRequirement::Disabled);
}
normalized
}
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(),
}
}
fn format_network_constraints(network: &NetworkConstraints) -> String {
let mut parts = Vec::new();
let NetworkConstraints {
enabled,
http_port,
socks_port,
allow_upstream_proxy,
dangerously_allow_non_loopback_proxy,
dangerously_allow_all_unix_sockets,
domains,
review_domains,
managed_allowed_domains_only,
unix_sockets,
allow_local_binding,
header_injections,
} = network;
if let Some(enabled) = enabled {
parts.push(format!("enabled={enabled}"));
}
if let Some(http_port) = http_port {
parts.push(format!("http_port={http_port}"));
}
if let Some(socks_port) = socks_port {
parts.push(format!("socks_port={socks_port}"));
}
if let Some(allow_upstream_proxy) = allow_upstream_proxy {
parts.push(format!("allow_upstream_proxy={allow_upstream_proxy}"));
}
if let Some(dangerously_allow_non_loopback_proxy) = dangerously_allow_non_loopback_proxy {
parts.push(format!(
"dangerously_allow_non_loopback_proxy={dangerously_allow_non_loopback_proxy}"
));
}
if let Some(dangerously_allow_all_unix_sockets) = dangerously_allow_all_unix_sockets {
parts.push(format!(
"dangerously_allow_all_unix_sockets={dangerously_allow_all_unix_sockets}"
));
}
if let Some(domains) = domains {
parts.push(format!(
"domains={}",
format_network_permission_entries(&domains.entries, format_network_domain_permission)
));
}
if let Some(review_domains) = review_domains {
parts.push(format!("review_domains={{{}}}", review_domains.join(", ")));
}
if let Some(managed_allowed_domains_only) = managed_allowed_domains_only {
parts.push(format!(
"managed_allowed_domains_only={managed_allowed_domains_only}"
));
}
if let Some(unix_sockets) = unix_sockets {
parts.push(format!(
"unix_sockets={}",
format_network_permission_entries(
&unix_sockets.entries,
format_network_unix_socket_permission,
)
));
}
if let Some(allow_local_binding) = allow_local_binding {
parts.push(format!("allow_local_binding={allow_local_binding}"));
}
if let Some(header_injections) = header_injections {
let hosts = header_injections
.iter()
.map(|rule| rule.host.as_str())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
.join(", ");
parts.push(format!(
"header_injections={} hosts={{{hosts}}}",
header_injections.len()
));
}
join_or_empty(parts)
}
fn format_network_permission_entries<T: Copy>(
entries: &std::collections::BTreeMap<String, T>,
format_value: impl Fn(T) -> &'static str,
) -> String {
let parts = entries
.iter()
.map(|(key, value)| format!("{key}={}", format_value(*value)))
.collect::<Vec<_>>();
format!("{{{}}}", parts.join(", "))
}
fn format_network_domain_permission(permission: NetworkDomainPermissionToml) -> &'static str {
match permission {
NetworkDomainPermissionToml::Allow => "allow",
NetworkDomainPermissionToml::Deny => "deny",
}
}
fn format_network_unix_socket_permission(
permission: NetworkUnixSocketPermissionToml,
) -> &'static str {
match permission {
NetworkUnixSocketPermissionToml::Allow => "allow",
NetworkUnixSocketPermissionToml::Deny => "deny",
}
}
#[cfg(test)]
mod tests {
use super::render_agents_config_lines;
use super::render_debug_config_lines;
use super::sandbox_mode_is_allowed_by_permissions;
use super::session_all_proxy_url;
use crate::legacy_core::config::ConfigBuilder;
use crate::legacy_core::config::Permissions;
use codex_app_server_protocol::AskForApproval;
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerSource;
use codex_config::ConfigLayerStack;
use codex_config::ConfigRequirements;
use codex_config::ConfigRequirementsToml;
use codex_config::ConfigRequirementsWithSources;
use codex_config::Constrained;
use codex_config::ConstrainedWithSource;
use codex_config::ConstraintError;
use codex_config::FeatureRequirementsToml;
use codex_config::FilesystemConstraints;
use codex_config::HookEventsToml;
use codex_config::HookHandlerConfig;
use codex_config::LoaderOverrides;
use codex_config::ManagedHooksRequirementsToml;
use codex_config::MatcherGroup;
use codex_config::NetworkConstraints;
use codex_config::NetworkDomainPermissionToml;
use codex_config::NetworkDomainPermissionsToml;
use codex_config::NetworkHeaderInjectionToml;
use codex_config::NetworkUnixSocketPermissionToml;
use codex_config::NetworkUnixSocketPermissionsToml;
use codex_config::RequirementSource;
use codex_config::ResidencyRequirement;
use codex_config::SandboxModeRequirement;
use codex_config::Sourced;
use codex_config::WebSearchModeRequirement;
use codex_config::WindowsRequirementsToml;
use codex_config::sandbox_mode_requirement_for_permission_profile;
use codex_config::types::FeedbackConfigToml;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use ratatui::text::Line;
use std::collections::BTreeMap;
use toml::Value as TomlValue;
#[tokio::test]
async fn debug_config_output_lists_agents_fields() {
let codex_home = tempfile::tempdir().expect("create temp dir");
std::fs::write(
codex_home.path().join(codex_config::CONFIG_TOML_FILE),
r#"[agents]
enabled = false
max_concurrent_threads_per_session = 7
max_depth = -2
default_subagent_model = "gpt-5.6-terra"
default_subagent_reasoning_effort = "high"
interrupt_message = false
"#,
)
.expect("write config");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.loader_overrides(LoaderOverrides::without_managed_config_for_tests())
.build()
.await
.expect("load config");
insta::assert_snapshot!(render_to_text(&render_agents_config_lines(&config)));
}
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")
}
fn render_stack_to_text(stack: &ConfigLayerStack) -> String {
render_stack_to_text_with_sandbox_mode_filter(stack, |_| true)
}
fn render_stack_to_text_with_sandbox_mode_filter(
stack: &ConfigLayerStack,
sandbox_mode_is_effectively_allowed: impl Fn(SandboxModeRequirement) -> bool,
) -> String {
render_to_text(&render_debug_config_lines(
stack,
sandbox_mode_is_effectively_allowed,
))
}
#[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_stack_to_text(&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:\\ProgramData\\OpenAI\\Codex\\requirements.toml")
} else {
absolute_path("/etc/codex/requirements.toml")
};
let denied_path = if cfg!(windows) {
absolute_path("C:\\Users\\alice\\.gitconfig")
} else {
absolute_path("/home/alice/.gitconfig")
};
let sqlite_home = if cfg!(windows) {
absolute_path("C:\\Users\\alice\\.codex\\state")
} else {
absolute_path("/home/alice/.codex/state")
};
let log_dir = if cfg!(windows) {
absolute_path("C:\\Users\\alice\\.codex\\logs")
} else {
absolute_path("/home/alice/.codex/logs")
};
let model_catalog_json = if cfg!(windows) {
absolute_path("C:\\Users\\alice\\.codex\\models.json")
} else {
absolute_path("/home/alice/.codex/models.json")
};
let requirements = ConfigRequirements {
sqlite_home: Some(Sourced::new(
sqlite_home.clone(),
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
log_dir: Some(Sourced::new(
log_dir.clone(),
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
model_catalog_json: Some(Sourced::new(
model_catalog_json.clone(),
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
check_for_update_on_startup: Some(Sourced::new(
/*value*/ false,
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
allow_login_shell: Some(Sourced::new(
/*value*/ false,
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
feedback: Some(Sourced::new(
FeedbackConfigToml {
enabled: Some(false),
},
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
approval_policy: ConstrainedWithSource::new(
Constrained::allow_any(AskForApproval::OnRequest.to_core()),
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
),
approvals_reviewer: ConstrainedWithSource::new(
Constrained::allow_any(ApprovalsReviewer::AutoReview),
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
),
permission_profile: ConstrainedWithSource::new(
Constrained::allow_any(PermissionProfile::read_only()),
Some(RequirementSource::SystemRequirementsToml {
file: requirements_file.clone(),
}),
),
enforce_residency: ConstrainedWithSource::new(
Constrained::allow_any(Some(ResidencyRequirement::Us)),
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
),
web_search_mode: ConstrainedWithSource::new(
Constrained::allow_any(WebSearchMode::Cached),
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
),
allow_managed_hooks_only: Some(Sourced::new(
/*value*/ true,
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
allow_appshots: Some(Sourced::new(
/*value*/ false,
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
allow_remote_control: Some(Sourced::new(
/*value*/ false,
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
feature_requirements: Some(Sourced::new(
FeatureRequirementsToml {
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
..Default::default()
},
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
network: Some(Sourced::new(
NetworkConstraints {
enabled: Some(true),
domains: Some(NetworkDomainPermissionsToml {
entries: BTreeMap::from([(
"example.com".to_string(),
NetworkDomainPermissionToml::Allow,
)]),
}),
review_domains: Some(vec!["api.example.com".to_string()]),
header_injections: Some(vec![NetworkHeaderInjectionToml {
host: "api.example.com".to_string(),
methods: vec!["POST".to_string()],
path_prefixes: vec!["/v1".to_string()],
headers: BTreeMap::from([(
"x-managed-source".to_string(),
"secret-looking-value".to_string(),
)]),
}]),
..Default::default()
},
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
filesystem: Some(Sourced::new(
FilesystemConstraints {
deny_read: vec![denied_path.clone().into()],
},
RequirementSource::SystemRequirementsToml {
file: requirements_file.clone(),
},
)),
guardian_policy_config_source: Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
..ConfigRequirements::default()
};
let requirements_toml = ConfigRequirementsToml {
config_server: None,
application: None,
allowed_login_methods: None,
allowed_chatgpt_workspaces: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
sqlite_home: Some(sqlite_home),
unsafe_enforce_sqlite_journal_mode: None,
log_dir: Some(log_dir),
model_catalog_json: Some(model_catalog_json),
model_provider: None,
model_providers: None,
check_for_update_on_startup: Some(false),
allow_login_shell: Some(false),
feedback: Some(FeedbackConfigToml {
enabled: Some(false),
}),
allowed_approval_policies: Some(vec![AskForApproval::OnRequest.to_core()]),
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
allowed_permission_profiles: None,
default_permissions: None,
remote_sandbox_config: None,
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
allow_managed_hooks_only: Some(true),
allow_appshots: Some(false),
allow_remote_control: Some(false),
allow_browser_and_computer_use: None,
computer_use: None,
browser_use: None,
in_app_browser: None,
windows: Some(WindowsRequirementsToml {
allowed_sandbox_implementations: None,
allow_mxc: None,
require_mxc: None,
}),
additional_developer_instructions: None,
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
guardian_extra_policy: None,
feature_requirements: Some(FeatureRequirementsToml {
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
..Default::default()
}),
hooks: None,
mcp_servers: None,
plugins: None,
marketplaces: None,
otel: None,
apps: None,
rules: None,
enforce_residency: Some(ResidencyRequirement::Us),
network: None,
permissions: None,
auto_review: None,
models: None,
};
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,
profile: None,
},
empty_toml_table(),
)],
requirements,
requirements_toml,
)
.expect("config layer stack");
let rendered = render_stack_to_text(&stack);
#[cfg(not(windows))]
insta::assert_snapshot!("debug_config_requirement_sources", rendered.as_str());
let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
assert!(rendered.contains(&format!(
"allowed_approval_policies: on-request (source: {requirements_source})"
)));
assert!(rendered.contains(
"allowed_approvals_reviewers: auto_review (source: MDM managed_config.toml (legacy))"
));
assert!(
rendered.contains(
format!(
"allowed_sandbox_modes: read-only (source: {})",
requirements_file.as_path().display()
)
.as_str(),
)
);
assert!(rendered.contains(&format!(
"allowed_web_search_modes: cached, disabled (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"allow_managed_hooks_only: true (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"allow_appshots: false (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"allow_remote_control: false (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"guardian_policy_config: configured (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"features: guardian_approval=true (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"enforce_residency: us (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"network_proxy: enabled=true, domains={{example.com=allow}}, review_domains={{api.example.com}}, header_injections=1 hosts={{api.example.com}} (source: {requirements_source})"
)));
assert!(!rendered.contains("secret-looking-value"));
assert!(
rendered.contains(
format!(
"permissions.filesystem.deny_read: {}",
denied_path.as_path().display()
)
.as_str()
)
);
assert!(!rendered.contains(" - rules:"));
}
#[test]
fn debug_config_output_filters_sandbox_modes_blocked_by_deny_read_requirements() {
let requirements_file = if cfg!(windows) {
absolute_path("C:\\ProgramData\\OpenAI\\Codex\\requirements.toml")
} else {
absolute_path("/etc/codex/requirements.toml")
};
let denied_path = if cfg!(windows) {
absolute_path("C:\\Users\\alice\\.gitconfig")
} else {
absolute_path("/home/alice/.gitconfig")
};
let requirements = ConfigRequirements {
permission_profile: ConstrainedWithSource::new(
Constrained::allow_any(PermissionProfile::read_only()),
Some(RequirementSource::SystemRequirementsToml {
file: requirements_file.clone(),
}),
),
filesystem: Some(Sourced::new(
FilesystemConstraints {
deny_read: vec![denied_path.into()],
},
RequirementSource::SystemRequirementsToml {
file: requirements_file.clone(),
},
)),
..ConfigRequirements::default()
};
let requirements_toml = ConfigRequirementsToml {
allowed_sandbox_modes: Some(vec![
SandboxModeRequirement::ReadOnly,
SandboxModeRequirement::WorkspaceWrite,
SandboxModeRequirement::DangerFullAccess,
SandboxModeRequirement::ExternalSandbox,
]),
..ConfigRequirementsToml::default()
};
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let constrained_permission_profile =
Constrained::new(PermissionProfile::read_only(), |candidate| {
let mode = sandbox_mode_requirement_for_permission_profile(candidate);
match mode {
SandboxModeRequirement::ReadOnly | SandboxModeRequirement::WorkspaceWrite => {
Ok(())
}
SandboxModeRequirement::DangerFullAccess
| SandboxModeRequirement::ExternalSandbox => {
Err(ConstraintError::InvalidValue {
field_name: "sandbox_mode",
candidate: format!("{mode:?}"),
allowed: "[read-only, workspace-write]".to_string(),
requirement_source: RequirementSource::Unknown,
})
}
}
})
.expect("constrained permission profile");
let permissions = Permissions::from_approval_and_profile(
Constrained::allow_any(AskForApproval::OnRequest.to_core()),
constrained_permission_profile,
)
.expect("permissions");
let rendered = render_stack_to_text_with_sandbox_mode_filter(&stack, |mode| {
sandbox_mode_is_allowed_by_permissions(&permissions, mode)
});
#[cfg(not(windows))]
insta::assert_snapshot!(
"debug_config_effective_sandbox_modes_with_deny_read",
rendered.as_str()
);
assert!(
rendered.contains(
format!(
"allowed_sandbox_modes: read-only, workspace-write (source: {})",
requirements_file.as_path().display()
)
.as_str()
)
);
assert!(!rendered.contains("danger-full-access"));
assert!(!rendered.contains("external-sandbox"));
}
#[test]
fn debug_config_output_lists_approvals_reviewer_as_requirement() {
let requirements = ConfigRequirements {
approvals_reviewer: ConstrainedWithSource::new(
Constrained::allow_any(ApprovalsReviewer::AutoReview),
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
),
..ConfigRequirements::default()
};
let requirements_toml = ConfigRequirementsToml {
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
..ConfigRequirementsToml::default()
};
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let rendered = render_stack_to_text(&stack);
assert!(rendered.contains(
"allowed_approvals_reviewers: auto_review (source: MDM managed_config.toml (legacy))"
));
assert!(!rendered.contains("Requirements:\n <none>"));
}
#[test]
fn debug_config_output_formats_unix_socket_permissions() {
let requirements = ConfigRequirements {
network: Some(Sourced::new(
NetworkConstraints {
unix_sockets: Some(NetworkUnixSocketPermissionsToml {
entries: BTreeMap::from([
(
"/tmp/codex.sock".to_string(),
NetworkUnixSocketPermissionToml::Allow,
),
(
"/tmp/blocked.sock".to_string(),
NetworkUnixSocketPermissionToml::Deny,
),
]),
}),
..Default::default()
},
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
..ConfigRequirements::default()
};
let stack =
ConfigLayerStack::new(Vec::new(), requirements, ConfigRequirementsToml::default())
.expect("config layer stack");
let rendered = render_stack_to_text(&stack);
let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
assert!(rendered.contains(&format!(
"network_proxy: unix_sockets={{/tmp/blocked.sock=deny, /tmp/codex.sock=allow}} (source: {requirements_source})"
)));
}
#[test]
fn debug_config_network_requirement_surfaces() {
let mut outputs = Vec::new();
for table in [
"network_proxy",
"experimental_network",
"permissions.network",
] {
let toml: ConfigRequirementsToml =
toml::from_str(&format!("[{table}]\nenabled = true")).expect("requirements");
let mut sourced = ConfigRequirementsWithSources::default();
sourced.merge_unset_fields(RequirementSource::Unknown, toml.clone());
let requirements = ConfigRequirements::try_from(sourced).expect("valid requirements");
let stack =
ConfigLayerStack::new(Vec::new(), requirements, toml).expect("config layer stack");
outputs.push(format!("{table}:\n{}", render_stack_to_text(&stack)));
}
insta::assert_snapshot!(outputs.join("\n\n"));
}
#[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_stack_to_text(&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 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(),
)
.expect("config layer stack");
let rendered = render_stack_to_text(&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_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_stack_to_text(&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 {
web_search_mode: ConstrainedWithSource::new(
Constrained::allow_any(WebSearchMode::Disabled),
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
),
..ConfigRequirements::default()
};
let requirements_toml = ConfigRequirementsToml {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
allowed_permission_profiles: None,
default_permissions: None,
remote_sandbox_config: None,
allowed_web_search_modes: Some(Vec::new()),
allow_managed_hooks_only: None,
allow_appshots: None,
allow_remote_control: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
guardian_extra_policy: None,
feature_requirements: None,
hooks: None,
mcp_servers: None,
plugins: None,
marketplaces: None,
apps: None,
rules: None,
enforce_residency: None,
network: None,
permissions: None,
models: None,
..ConfigRequirementsToml::default()
};
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let rendered = render_stack_to_text(&stack);
let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
assert!(rendered.contains(&format!(
"allowed_web_search_modes: disabled (source: {requirements_source})"
)));
}
#[test]
fn debug_config_output_lists_managed_hooks_requirement() {
let requirements = ConfigRequirements {
managed_hooks: Some(ConstrainedWithSource::new(
Constrained::allow_any(ManagedHooksRequirementsToml {
managed_dir: Some(if cfg!(windows) {
std::path::PathBuf::from(r"C:\enterprise\hooks")
} else {
std::path::PathBuf::from("/enterprise/hooks")
}),
windows_managed_dir: Some(std::path::PathBuf::from(r"C:\enterprise\hooks")),
hooks: Box::new(HookEventsToml {
pre_tool_use: vec![MatcherGroup {
matcher: Some("^Bash$".to_string()),
hooks: vec![HookHandlerConfig::Command {
command: "python3 /enterprise/hooks/pre.py".to_string(),
command_windows: None,
timeout_sec: Some(10),
r#async: false,
status_message: Some("checking".to_string()),
additional_context_limit: None,
}],
}],
..Default::default()
}),
}),
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
)),
..ConfigRequirements::default()
};
let requirements_toml = ConfigRequirementsToml {
hooks: requirements
.managed_hooks
.as_ref()
.map(|hooks| hooks.get().clone()),
..ConfigRequirementsToml::default()
};
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let rendered = render_stack_to_text(&stack);
let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
assert!(rendered.contains("hooks:"));
assert!(rendered.contains("handlers=1"));
assert!(rendered.contains(&format!("(source: {requirements_source})")));
}
#[test]
fn session_all_proxy_url_uses_socks_when_enabled() {
assert_eq!(
session_all_proxy_url(
"127.0.0.1:3128",
"127.0.0.1:8081",
/*socks_enabled*/ true
),
"socks5h://127.0.0.1:8081".to_string()
);
}
#[test]
fn session_all_proxy_url_uses_http_when_socks_disabled() {
assert_eq!(
session_all_proxy_url(
"127.0.0.1:3128",
"127.0.0.1:8081",
/*socks_enabled*/ false
),
"http://127.0.0.1:3128".to_string()
);
}
}