mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
8534912df9
## 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`.
1209 lines
43 KiB
Rust
1209 lines
43 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_app_server_protocol::ConfigLayerSource;
|
|
use codex_config::CONFIG_TOML_FILE;
|
|
use codex_config::ConfigLayerEntry;
|
|
use codex_config::ConfigLayerStack;
|
|
use codex_config::ConfigLayerStackOrdering;
|
|
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)
|
|
});
|
|
|
|
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 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 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, 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(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 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()),
|
|
));
|
|
}
|
|
|
|
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::EnterpriseManaged { .. }
|
|
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => render_non_file_layer_value(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 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::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,
|
|
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::Deny => "deny",
|
|
}
|
|
}
|
|
|
|
#[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;
|
|
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::HookEventsToml;
|
|
use codex_config::HookHandlerConfig;
|
|
use codex_config::ManagedHooksRequirementsToml;
|
|
use codex_config::MatcherGroup;
|
|
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_config::sandbox_mode_requirement_for_permission_profile;
|
|
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;
|
|
|
|
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 requirements = ConfigRequirements {
|
|
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(),
|
|
}),
|
|
),
|
|
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::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,
|
|
)),
|
|
feature_requirements: Some(Sourced::new(
|
|
FeatureRequirementsToml {
|
|
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
|
|
},
|
|
RequirementSource::LegacyManagedConfigTomlFromMdm,
|
|
)),
|
|
network: Some(Sourced::new(
|
|
NetworkConstraints {
|
|
enabled: Some(true),
|
|
domains: Some(NetworkDomainPermissionsToml {
|
|
entries: BTreeMap::from([(
|
|
"example.com".to_string(),
|
|
NetworkDomainPermissionToml::Allow,
|
|
)]),
|
|
}),
|
|
..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 {
|
|
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),
|
|
computer_use: None,
|
|
windows: None,
|
|
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
|
|
feature_requirements: Some(FeatureRequirementsToml {
|
|
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
|
|
}),
|
|
hooks: None,
|
|
mcp_servers: Some(BTreeMap::from([(
|
|
"docs".to_string(),
|
|
McpServerRequirement {
|
|
identity: McpServerIdentity::Command {
|
|
command: "codex-mcp".to_string(),
|
|
},
|
|
},
|
|
)])),
|
|
plugins: None,
|
|
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,
|
|
profile: None,
|
|
},
|
|
empty_toml_table(),
|
|
)],
|
|
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_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!(
|
|
"guardian_policy_config: configured (source: {requirements_source})"
|
|
)));
|
|
assert!(rendered.contains(&format!(
|
|
"features: guardian_approval=true (source: {requirements_source})"
|
|
)));
|
|
assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))"));
|
|
assert!(rendered.contains(&format!(
|
|
"enforce_residency: us (source: {requirements_source})"
|
|
)));
|
|
assert!(rendered.contains(&format!(
|
|
"experimental_network: enabled=true, domains={{example.com=allow}} (source: {requirements_source})"
|
|
)));
|
|
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!(
|
|
"experimental_network: unix_sockets={{/tmp/blocked.sock=deny, /tmp/codex.sock=allow}} (source: {requirements_source})"
|
|
)));
|
|
}
|
|
|
|
#[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,
|
|
computer_use: None,
|
|
windows: None,
|
|
guardian_policy_config: None,
|
|
feature_requirements: None,
|
|
hooks: None,
|
|
mcp_servers: None,
|
|
plugins: 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_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: 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()),
|
|
}],
|
|
}],
|
|
..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()
|
|
);
|
|
}
|
|
}
|