mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
4ca2e436e5
commit
8534912df9
@@ -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"));
|
||||
|
||||
+12
@@ -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)
|
||||
Reference in New Issue
Block a user