Show effective sandbox modes in /debug-config (#27068)

## Summary
- Render `/debug-config`'s `allowed_sandbox_modes` from the finalized
permission constraints instead of the raw requirements list.
- Add regression coverage for configured full-access and external
sandbox modes being omitted when effective permissions reject them.

## Details
`allowed_sandbox_modes` comes from managed requirements, but the final
permissions can be further constrained by derived validation rules. For
example, `permissions.filesystem.deny_read` requires sandbox
enforcement, so modes that disable or externalize Codex's sandbox are
not actually usable even if they were present in the raw requirements
TOML.

The debug renderer now enumerates the configured sandbox-mode labels and
keeps only those accepted by `Config.permissions`. That makes
`/debug-config` reflect the same effective permission-profile constraint
path used by runtime config validation, while preserving the existing
source/provenance display.

## Validation
- Added a regression test for effective sandbox-mode filtering in
`/debug-config`.
This commit is contained in:
canvrno-oai
2026-06-08 17:03:52 -07:00
committed by GitHub
Unverified
parent 4ca2e436e5
commit 8534912df9
2 changed files with 155 additions and 11 deletions
+143 -11
View File
@@ -1,5 +1,6 @@
use crate::history_cell::PlainHistoryCell;
use crate::legacy_core::config::Config;
use crate::legacy_core::config::Permissions;
use crate::session_state::SessionNetworkProxyRuntime;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::CONFIG_TOML_FILE;
@@ -15,6 +16,8 @@ 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;
@@ -23,7 +26,9 @@ 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);
let mut lines = render_debug_config_lines(&config.config_layer_stack, |mode| {
sandbox_mode_is_allowed_by_permissions(&config.permissions, mode)
});
if let Some(proxy) = session_network_proxy {
lines.push("".into());
@@ -49,6 +54,24 @@ pub(crate) fn new_debug_config_output(
PlainHistoryCell::new(lines)
}
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}")
@@ -57,7 +80,10 @@ fn session_all_proxy_url(http_addr: &str, socks_addr: &str, socks_enabled: bool)
}
}
fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
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(
@@ -122,6 +148,7 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
modes
.iter()
.copied()
.filter(|mode| sandbox_mode_is_effectively_allowed(*mode))
.map(format_sandbox_mode_requirement)
.collect::<Vec<_>>(),
);
@@ -515,8 +542,11 @@ fn format_network_unix_socket_permission(
#[cfg(test)]
mod tests {
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::Constrained;
use crate::legacy_core::config::ConstraintError;
use crate::legacy_core::config::Permissions;
use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::ConfigLayerEntry;
@@ -542,6 +572,7 @@ mod tests {
use codex_config::SandboxModeRequirement;
use codex_config::Sourced;
use codex_config::WebSearchModeRequirement;
use codex_config::sandbox_mode_requirement_for_permission_profile;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::PermissionProfile;
@@ -571,6 +602,20 @@ mod tests {
.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) {
@@ -604,7 +649,7 @@ mod tests {
)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
let rendered = render_stack_to_text(&stack);
assert!(rendered.contains("(enabled)"));
assert!(rendered.contains("(disabled)"));
assert!(rendered.contains("reason: project is untrusted"));
@@ -749,7 +794,7 @@ mod tests {
)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
let rendered = render_stack_to_text(&stack);
let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
assert!(rendered.contains(&format!(
"allowed_approval_policies: on-request (source: {requirements_source})"
@@ -800,6 +845,93 @@ mod tests {
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 {
@@ -816,7 +948,7 @@ mod tests {
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
let rendered = render_stack_to_text(&stack);
assert!(rendered.contains(
"allowed_approvals_reviewers: auto_review (source: MDM managed_config.toml (legacy))"
));
@@ -851,7 +983,7 @@ mod tests {
ConfigLayerStack::new(Vec::new(), requirements, ConfigRequirementsToml::default())
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
let rendered = render_stack_to_text(&stack);
let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
assert!(rendered.contains(&format!(
"experimental_network: unix_sockets={{/tmp/blocked.sock=deny, /tmp/codex.sock=allow}} (source: {requirements_source})"
@@ -880,7 +1012,7 @@ writable_roots = ["/tmp"]
)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&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"));
@@ -914,7 +1046,7 @@ approval_policy = "never"
)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&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"));
@@ -951,7 +1083,7 @@ approval_policy = "never"
)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&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:"));
@@ -997,7 +1129,7 @@ approval_policy = "never"
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&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})"
@@ -1043,7 +1175,7 @@ approval_policy = "never"
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
let rendered = render_stack_to_text(&stack);
let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
assert!(rendered.contains("hooks:"));
assert!(rendered.contains("handlers=1"));
@@ -0,0 +1,12 @@
---
source: tui/src/debug_config.rs
expression: rendered.as_str()
---
/debug-config
Config layer stack (lowest precedence first):
<none>
Requirements:
- allowed_sandbox_modes: read-only, workspace-write (source: /etc/codex/requirements.toml)
- permissions.filesystem.deny_read: /home/alice/.gitconfig (source: /etc/codex/requirements.toml)