Files
codex/codex-rs/tui/src/debug_config.rs
Abhinav ab26554a3a Add remote_sandbox_config to our config requirements (#18763)
## Why

Customers need finer-grained control over allowed sandbox modes based on
the host Codex is running on. For example, they may want stricter
sandbox limits on devboxes while keeping a different default elsewhere.

Our current cloud requirements can target user/account groups, but they
cannot vary sandbox requirements by host. That makes remote development
environments awkward because the same top-level `allowed_sandbox_modes`
has to apply everywhere.

## What

Adds a new `remote_sandbox_config` section to `requirements.toml`:

```toml
allowed_sandbox_modes = ["read-only"]

[[remote_sandbox_config]]
hostname_patterns = ["*.org"]
allowed_sandbox_modes = ["read-only", "workspace-write"]

[[remote_sandbox_config]]
hostname_patterns = ["*.sh", "runner-*.ci"]
allowed_sandbox_modes = ["read-only", "danger-full-access"]
```

During requirements resolution, Codex resolves the local host name once,
preferring the machine FQDN when available and falling back to the
cleaned kernel hostname. This host classification is best effort rather
than authenticated device proof.

Each requirements source applies its first matching
`remote_sandbox_config` entry before it is merged with other sources.
The shared merge helper keeps that `apply_remote_sandbox_config` step
paired with requirements merging so new requirements sources do not have
to remember the extra call.

That preserves source precedence: a lower-precedence requirements file
with a matching `remote_sandbox_config` cannot override a
higher-precedence source that already set `allowed_sandbox_modes`.

This also wires the hostname-aware resolution through app-server,
CLI/TUI config loading, config API reads, and config layer metadata so
they all evaluate remote sandbox requirements consistently.

## Verification

- `cargo test -p codex-config remote_sandbox_config`
- `cargo test -p codex-config host_name`
- `cargo test -p codex-core
load_config_layers_applies_matching_remote_sandbox_config`
- `cargo test -p codex-core
system_remote_sandbox_config_keeps_cloud_sandbox_modes`
- `cargo test -p codex-config`
- `cargo test -p codex-core` unit tests passed; `tests/all.rs`
integration matrix was intentionally stopped after the relevant focused
tests passed
- `just fix -p codex-config`
- `just fix -p codex-core`
- `cargo check -p codex-app-server`
2026-04-21 05:05:02 +00:00

892 lines
31 KiB
Rust

use crate::history_cell::PlainHistoryCell;
use crate::legacy_core::config::Config;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerStack;
use codex_config::ConfigLayerStackOrdering;
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_protocol::protocol::SessionNetworkProxyRuntime;
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);
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 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) -> 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,
/*include_disabled*/ 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());
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(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()
.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(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(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(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 let Some(network) = requirements.network.as_ref() {
requirement_lines.push(requirement_line(
"experimental_network",
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::LegacyManagedConfigTomlFromMdm => {
render_mdm_layer_details(layer)
}
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 render_mdm_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>> {
let value = layer
.raw_toml()
.map(ToString::to_string)
.unwrap_or_else(|| format_toml_value(&layer.config));
if value.is_empty() {
return vec![" MDM value: <empty>".dim().into()];
}
if value.contains('\n') {
let mut lines = vec![" MDM value:".into()];
lines.extend(value.lines().map(|line| format!(" {line}").into()));
lines
} else {
vec![format!(" MDM value: {value}").into()]
}
}
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_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(),
}
}
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,
managed_allowed_domains_only,
unix_sockets,
allow_local_binding,
} = 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(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}"));
}
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::None => "none",
}
}
#[cfg(test)]
mod tests {
use super::render_debug_config_lines;
use super::session_all_proxy_url;
use crate::legacy_core::config::Constrained;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerStack;
use codex_config::ConfigRequirements;
use codex_config::ConfigRequirementsToml;
use codex_config::ConstrainedWithSource;
use codex_config::FeatureRequirementsToml;
use codex_config::FilesystemConstraints;
use codex_config::McpServerIdentity;
use codex_config::McpServerRequirement;
use codex_config::NetworkConstraints;
use codex_config::NetworkDomainPermissionToml;
use codex_config::NetworkDomainPermissionsToml;
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_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::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:\\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 {
approval_policy: ConstrainedWithSource::new(
Constrained::allow_any(AskForApproval::OnRequest),
Some(RequirementSource::CloudRequirements),
),
approvals_reviewer: ConstrainedWithSource::new(
Constrained::allow_any(ApprovalsReviewer::GuardianSubagent),
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
),
sandbox_policy: ConstrainedWithSource::new(
Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
Some(RequirementSource::SystemRequirementsToml {
file: requirements_file.clone(),
}),
),
mcp_servers: Some(Sourced::new(
BTreeMap::from([(
"docs".to_string(),
McpServerRequirement {
identity: McpServerIdentity::Command {
command: "codex-mcp".to_string(),
},
},
)]),
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
enforce_residency: ConstrainedWithSource::new(
Constrained::allow_any(Some(ResidencyRequirement::Us)),
Some(RequirementSource::CloudRequirements),
),
web_search_mode: ConstrainedWithSource::new(
Constrained::allow_any(WebSearchMode::Cached),
Some(RequirementSource::CloudRequirements),
),
feature_requirements: Some(Sourced::new(
FeatureRequirementsToml {
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
},
RequirementSource::CloudRequirements,
)),
network: Some(Sourced::new(
NetworkConstraints {
enabled: Some(true),
domains: Some(NetworkDomainPermissionsToml {
entries: BTreeMap::from([(
"example.com".to_string(),
NetworkDomainPermissionToml::Allow,
)]),
}),
..Default::default()
},
RequirementSource::CloudRequirements,
)),
filesystem: Some(Sourced::new(
FilesystemConstraints {
deny_read: vec![denied_path.clone().into()],
},
RequirementSource::SystemRequirementsToml {
file: requirements_file.clone(),
},
)),
..ConfigRequirements::default()
};
let requirements_toml = ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::GuardianSubagent]),
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
remote_sandbox_config: None,
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
guardian_policy_config: None,
feature_requirements: Some(FeatureRequirementsToml {
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
}),
mcp_servers: Some(BTreeMap::from([(
"docs".to_string(),
McpServerRequirement {
identity: McpServerIdentity::Command {
command: "codex-mcp".to_string(),
},
},
)])),
apps: None,
rules: None,
enforce_residency: Some(ResidencyRequirement::Us),
network: None,
permissions: 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 },
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(
"allowed_approvals_reviewers: guardian_subagent (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(
"allowed_web_search_modes: cached, disabled (source: cloud requirements)"
)
);
assert!(rendered.contains("features: guardian_approval=true (source: cloud requirements)"));
assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))"));
assert!(rendered.contains("enforce_residency: us (source: cloud requirements)"));
assert!(rendered.contains(
"experimental_network: enabled=true, domains={example.com=allow} (source: cloud requirements)"
));
assert!(
rendered.contains(
format!(
"permissions.filesystem.deny_read: {}",
denied_path.as_path().display()
)
.as_str()
)
);
assert!(!rendered.contains(" - rules:"));
}
#[test]
fn debug_config_output_lists_approvals_reviewer_as_requirement() {
let requirements = ConfigRequirements {
approvals_reviewer: ConstrainedWithSource::new(
Constrained::allow_any(ApprovalsReviewer::GuardianSubagent),
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
),
..ConfigRequirements::default()
};
let requirements_toml = ConfigRequirementsToml {
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::GuardianSubagent]),
..ConfigRequirementsToml::default()
};
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
assert!(rendered.contains(
"allowed_approvals_reviewers: guardian_subagent (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::None,
),
]),
}),
..Default::default()
},
RequirementSource::CloudRequirements,
)),
..ConfigRequirements::default()
};
let stack =
ConfigLayerStack::new(Vec::new(), requirements, ConfigRequirementsToml::default())
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
assert!(rendered.contains(
"experimental_network: unix_sockets={/tmp/blocked.sock=none, /tmp/codex.sock=allow} (source: cloud requirements)"
));
}
#[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_to_text(&render_debug_config_lines(&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 stack = ConfigLayerStack::new(
vec![ConfigLayerEntry::new_with_raw_toml(
ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
mdm_value,
raw_mdm_toml.to_string(),
)],
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&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_normalizes_empty_web_search_mode_list() {
let requirements = ConfigRequirements {
web_search_mode: ConstrainedWithSource::new(
Constrained::allow_any(WebSearchMode::Disabled),
Some(RequirementSource::CloudRequirements),
),
..ConfigRequirements::default()
};
let requirements_toml = ConfigRequirementsToml {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: Some(Vec::new()),
guardian_policy_config: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
rules: None,
enforce_residency: None,
network: None,
permissions: None,
};
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let rendered = render_to_text(&render_debug_config_lines(&stack));
assert!(
rendered.contains("allowed_web_search_modes: disabled (source: cloud requirements)")
);
}
#[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()
);
}
}