feat: include NetworkConfig through ExecParams

This commit is contained in:
Michael Bolin
2026-02-08 09:18:31 -08:00
parent 91a3e17960
commit fa48061df3
18 changed files with 257 additions and 130 deletions

5
codex-rs/Cargo.lock generated
View File

@@ -1565,6 +1565,7 @@ dependencies = [
"codex-file-search",
"codex-git",
"codex-keyring-store",
"codex-network-proxy",
"codex-otel",
"codex-protocol",
"codex-rmcp-client",
@@ -1901,9 +1902,8 @@ dependencies = [
"anyhow",
"async-trait",
"clap",
"codex-app-server-protocol",
"codex-core",
"codex-utils-absolute-path",
"codex-utils-home-dir",
"globset",
"pretty_assertions",
"rama-core",
@@ -1919,6 +1919,7 @@ dependencies = [
"tempfile",
"time",
"tokio",
"toml 0.9.11+spec-1.1.0",
"tracing",
"tracing-subscriber",
"url",

View File

@@ -93,6 +93,7 @@ codex-linux-sandbox = { path = "linux-sandbox" }
codex-lmstudio = { path = "lmstudio" }
codex-login = { path = "login" }
codex-mcp-server = { path = "mcp-server" }
codex-network-proxy = { path = "network-proxy" }
codex-ollama = { path = "ollama" }
codex-otel = { path = "otel" }
codex-process-hardening = { path = "process-hardening" }

View File

@@ -1628,6 +1628,7 @@ impl CodexMessageProcessor {
cwd,
expiration: timeout_ms.into(),
env,
network: self.config.network.clone(),
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level,
justification: None,

View File

@@ -35,6 +35,7 @@ codex-execpolicy = { workspace = true }
codex-file-search = { workspace = true }
codex-git = { workspace = true }
codex-keyring-store = { workspace = true }
codex-network-proxy = { workspace = true }
codex-otel = { workspace = true }
codex-protocol = { workspace = true }
codex-rmcp-client = { workspace = true }

View File

@@ -6658,6 +6658,7 @@ mod tests {
cwd: turn_context.cwd.clone(),
expiration: timeout_ms.into(),
env: HashMap::new(),
network: None,
sandbox_permissions,
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: Some("test".to_string()),
@@ -6670,6 +6671,7 @@ mod tests {
cwd: params.cwd.clone(),
expiration: timeout_ms.into(),
env: HashMap::new(),
network: None,
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: params.justification.clone(),
arg0: None,

View File

@@ -47,6 +47,7 @@ use crate::protocol::SandboxPolicy;
use crate::windows_sandbox::WindowsSandboxLevelExt;
use codex_app_server_protocol::Tools;
use codex_app_server_protocol::UserSavedConfig;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ModeKind;
@@ -152,6 +153,9 @@ pub struct Config {
/// using backend-specific headers or URLs to enforce this.
pub enforce_residency: Constrained<Option<ResidencyRequirement>>,
/// Effective network configuration applied to all spawned processes.
pub network: Option<NetworkProxy>,
/// True if the user passed in an override or set a value in config.toml
/// for either of approval_policy or sandbox_mode.
pub did_user_set_custom_approval_policy_or_sandbox_mode: bool,
@@ -1690,6 +1694,7 @@ impl Config {
approval_policy: constrained_approval_policy.value,
sandbox_policy: constrained_sandbox_policy.value,
enforce_residency: enforce_residency.value,
network: None,
did_user_set_custom_approval_policy_or_sandbox_mode,
forced_auto_mode_downgraded_on_windows,
shell_environment_policy,
@@ -4003,6 +4008,7 @@ model_verbosity = "high"
approval_policy: Constrained::allow_any(AskForApproval::Never),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
enforce_residency: Constrained::allow_any(None),
network: None,
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -4091,6 +4097,7 @@ model_verbosity = "high"
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
enforce_residency: Constrained::allow_any(None),
network: None,
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -4194,6 +4201,7 @@ model_verbosity = "high"
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
enforce_residency: Constrained::allow_any(None),
network: None,
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -4283,6 +4291,7 @@ model_verbosity = "high"
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
enforce_residency: Constrained::allow_any(None),
network: None,
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),

View File

@@ -32,6 +32,7 @@ use crate::sandboxing::SandboxPermissions;
use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
use crate::text_encoding::bytes_to_string_smart;
use codex_network_proxy::NetworkProxy;
use codex_utils_pty::process_group::kill_child_process_group;
pub const DEFAULT_EXEC_COMMAND_TIMEOUT_MS: u64 = 10_000;
@@ -63,6 +64,7 @@ pub struct ExecParams {
pub cwd: PathBuf,
pub expiration: ExecExpiration,
pub env: HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub sandbox_permissions: SandboxPermissions,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
pub justification: Option<String>,
@@ -170,12 +172,16 @@ pub async fn process_exec_tool_call(
command,
cwd,
expiration,
env,
mut env,
network,
sandbox_permissions,
windows_sandbox_level,
justification,
arg0: _,
} = params;
if let Some(network) = network.as_ref() {
network.apply_to_env(&mut env);
}
let (program, args) = command.split_first().ok_or_else(|| {
CodexErr::Io(io::Error::new(
@@ -233,6 +239,7 @@ pub(crate) async fn execute_exec_env(
cwd,
expiration,
env,
network: None,
sandbox_permissions,
windows_sandbox_level,
justification,
@@ -324,11 +331,15 @@ async fn exec_windows_sandbox(
let ExecParams {
command,
cwd,
env,
mut env,
network,
expiration,
windows_sandbox_level,
..
} = params;
if let Some(network) = network.as_ref() {
network.apply_to_env(&mut env);
}
// TODO(iceweasel-oai): run_windows_sandbox_capture should support all
// variants of ExecExpiration, not just timeout.
let timeout_ms = expiration.timeout_ms();
@@ -677,12 +688,16 @@ async fn exec(
let ExecParams {
command,
cwd,
env,
mut env,
network,
arg0,
expiration,
windows_sandbox_level: _,
..
} = params;
if let Some(network) = network.as_ref() {
network.apply_to_env(&mut env);
}
let (program, args) = command.split_first().ok_or_else(|| {
CodexErr::Io(io::Error::new(
@@ -1061,6 +1076,7 @@ mod tests {
cwd: std::env::current_dir()?,
expiration: 500.into(),
env,
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
justification: None,
@@ -1107,6 +1123,7 @@ mod tests {
cwd: cwd.clone(),
expiration: ExecExpiration::Cancellation(cancel_token),
env,
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
justification: None,

View File

@@ -430,8 +430,20 @@ mod tests {
}
}
fn can_bind_loopback_port() -> bool {
match std::net::TcpListener::bind(("127.0.0.1", 0)) {
Ok(_listener) => true,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => false,
Err(err) => panic!("unexpected error probing loopback listen permissions: {err}"),
}
}
#[tokio::test]
async fn refresh_available_models_sorts_by_priority() {
if !can_bind_loopback_port() {
return;
}
let server = MockServer::start().await;
let remote_models = vec![
remote_model("priority-low", "Low", 1),
@@ -489,6 +501,10 @@ mod tests {
#[tokio::test]
async fn refresh_available_models_uses_cache_when_fresh() {
if !can_bind_loopback_port() {
return;
}
let server = MockServer::start().await;
let remote_models = vec![remote_model("cached", "Cached", 5)];
let models_mock = mount_models_once(
@@ -536,6 +552,10 @@ mod tests {
#[tokio::test]
async fn refresh_available_models_refetches_when_cache_stale() {
if !can_bind_loopback_port() {
return;
}
let server = MockServer::start().await;
let initial_models = vec![remote_model("stale", "Stale", 1)];
let initial_mock = mount_models_once(
@@ -605,6 +625,10 @@ mod tests {
#[tokio::test]
async fn refresh_available_models_refetches_when_version_mismatch() {
if !can_bind_loopback_port() {
return;
}
let server = MockServer::start().await;
let initial_models = vec![remote_model("old", "Old", 1)];
let initial_mock = mount_models_once(
@@ -674,6 +698,10 @@ mod tests {
#[tokio::test]
async fn refresh_available_models_drops_removed_remote_models() {
if !can_bind_loopback_port() {
return;
}
let server = MockServer::start().await;
let initial_models = vec![remote_model("remote-old", "Remote Old", 1)];
let initial_mock = mount_models_once(

View File

@@ -53,6 +53,7 @@ impl ShellHandler {
cwd: turn_context.resolve_path(params.workdir.clone()),
expiration: params.timeout_ms.into(),
env: create_env(&turn_context.shell_environment_policy, Some(thread_id)),
network: turn_context.config.network.clone(),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: params.justification.clone(),
@@ -81,6 +82,7 @@ impl ShellCommandHandler {
cwd: turn_context.resolve_path(params.workdir.clone()),
expiration: params.timeout_ms.into(),
env: create_env(&turn_context.shell_environment_policy, Some(thread_id)),
network: turn_context.config.network.clone(),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: params.justification.clone(),
@@ -311,6 +313,7 @@ impl ShellHandler {
cwd: exec_params.cwd.clone(),
timeout_ms: exec_params.expiration.timeout_ms(),
env: exec_params.env.clone(),
network: exec_params.network.clone(),
sandbox_permissions: exec_params.sandbox_permissions,
justification: exec_params.justification.clone(),
exec_approval_requirement,
@@ -441,6 +444,7 @@ mod tests {
assert_eq!(exec_params.command, expected_command);
assert_eq!(exec_params.cwd, expected_cwd);
assert_eq!(exec_params.env, expected_env);
assert_eq!(exec_params.network, turn_context.config.network);
assert_eq!(exec_params.expiration.timeout_ms(), timeout_ms);
assert_eq!(exec_params.sandbox_permissions, sandbox_permissions);
assert_eq!(exec_params.justification, justification);

View File

@@ -23,6 +23,7 @@ use crate::tools::sandboxing::ToolCtx;
use crate::tools::sandboxing::ToolError;
use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::with_cached_approval;
use codex_network_proxy::NetworkProxy;
use codex_protocol::protocol::ReviewDecision;
use futures::future::BoxFuture;
use std::path::PathBuf;
@@ -33,6 +34,7 @@ pub struct ShellRequest {
pub cwd: PathBuf,
pub timeout_ms: Option<u64>,
pub env: std::collections::HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
pub exec_approval_requirement: ExecApprovalRequirement,
@@ -155,10 +157,14 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
command
};
let mut env = req.env.clone();
if let Some(network) = req.network.as_ref() {
network.apply_to_env(&mut env);
}
let spec = build_command_spec(
&command,
&req.cwd,
&req.env,
&env,
req.timeout_ms.into(),
req.sandbox_permissions,
req.justification.clone(),

View File

@@ -27,6 +27,7 @@ use crate::tools::sandboxing::with_cached_approval;
use crate::unified_exec::UnifiedExecError;
use crate::unified_exec::UnifiedExecProcess;
use crate::unified_exec::UnifiedExecProcessManager;
use codex_network_proxy::NetworkProxy;
use codex_protocol::protocol::ReviewDecision;
use futures::future::BoxFuture;
use std::collections::HashMap;
@@ -37,6 +38,7 @@ pub struct UnifiedExecRequest {
pub command: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub tty: bool,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
@@ -60,6 +62,7 @@ impl UnifiedExecRequest {
command: Vec<String>,
cwd: PathBuf,
env: HashMap<String, String>,
network: Option<NetworkProxy>,
tty: bool,
sandbox_permissions: SandboxPermissions,
justification: Option<String>,
@@ -69,6 +72,7 @@ impl UnifiedExecRequest {
command,
cwd,
env,
network,
tty,
sandbox_permissions,
justification,
@@ -181,10 +185,14 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
command
};
let mut env = req.env.clone();
if let Some(network) = req.network.as_ref() {
network.apply_to_env(&mut env);
}
let spec = build_command_spec(
&command,
&req.cwd,
&req.env,
&env,
ExecExpiration::DefaultTimeout,
req.sandbox_permissions,
req.justification.clone(),

View File

@@ -505,6 +505,7 @@ impl UnifiedExecProcessManager {
request.command.clone(),
cwd,
env,
context.turn.config.network.clone(),
request.tty,
request.sandbox_permissions,
request.justification.clone(),

View File

@@ -36,6 +36,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput
cwd: tmp.path().to_path_buf(),
expiration: 1000.into(),
env: HashMap::new(),
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
justification: None,

View File

@@ -87,6 +87,7 @@ impl EscalateServer {
cwd: PathBuf::from(&workdir),
expiration: ExecExpiration::Cancellation(cancel_rx),
env,
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
justification: None,

View File

@@ -75,6 +75,7 @@ async fn run_cmd_result_with_writable_roots(
cwd,
expiration: timeout_ms.into(),
env: create_env_from_core_vars(),
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
justification: None,
@@ -231,6 +232,7 @@ async fn assert_network_blocked(cmd: &[&str]) {
// do not stall the suite.
expiration: NETWORK_TIMEOUT_MS.into(),
env: create_env_from_core_vars(),
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
justification: None,

View File

@@ -19,14 +19,14 @@ workspace = true
anyhow = { workspace = true }
async-trait = { workspace = true }
clap = { workspace = true, features = ["derive"] }
codex-app-server-protocol = { workspace = true }
codex-core = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-home-dir = { workspace = true }
globset = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
time = { workspace = true }
tokio = { workspace = true, features = ["full"] }
toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["fmt"] }
url = { workspace = true }

View File

@@ -8,6 +8,7 @@ use crate::state::NetworkProxyState;
use anyhow::Context;
use anyhow::Result;
use clap::Parser;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::task::JoinHandle;
@@ -88,11 +89,42 @@ pub struct NetworkProxy {
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
}
impl std::fmt::Debug for NetworkProxy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Avoid logging internal state (config contents, derived globsets, etc.) which can be noisy
// and may contain sensitive paths.
f.debug_struct("NetworkProxy")
.field("http_addr", &self.http_addr)
.field("socks_addr", &self.socks_addr)
.field("admin_addr", &self.admin_addr)
.finish_non_exhaustive()
}
}
impl PartialEq for NetworkProxy {
fn eq(&self, other: &Self) -> bool {
self.http_addr == other.http_addr
&& self.socks_addr == other.socks_addr
&& self.admin_addr == other.admin_addr
}
}
impl Eq for NetworkProxy {}
impl NetworkProxy {
pub fn builder() -> NetworkProxyBuilder {
NetworkProxyBuilder::default()
}
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
// Enforce proxying for all child processes when configured. We always override to ensure
// the proxy is actually used even if the caller passed conflicting environment variables.
let proxy_url = format!("http://{}", self.http_addr);
for key in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"] {
env.insert(key.to_string(), proxy_url.clone());
}
}
pub async fn run(&self) -> Result<NetworkProxyHandle> {
let current_cfg = self.state.current_cfg().await?;
if !current_cfg.network.enabled {

View File

@@ -6,18 +6,17 @@ use crate::runtime::ConfigState;
use crate::runtime::LayerMtime;
use anyhow::Context;
use anyhow::Result;
use codex_app_server_protocol::ConfigLayerSource;
use codex_core::config::CONFIG_TOML_FILE;
use codex_core::config::ConstraintError;
use codex_core::config::find_codex_home;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::ConfigLayerStack;
use codex_core::config_loader::ConfigLayerStackOrdering;
use codex_core::config_loader::LoaderOverrides;
use codex_core::config_loader::RequirementSource;
use codex_core::config_loader::load_config_layers_state;
use codex_utils_home_dir::find_codex_home;
use serde::Deserialize;
use std::collections::HashSet;
use std::path::Path;
use tokio::fs;
use toml::Value as TomlValue;
const CONFIG_TOML_FILE: &str = "config.toml";
#[cfg(unix)]
const SYSTEM_CONFIG_TOML_FILE_UNIX: &str = "/etc/codex/config.toml";
pub use crate::runtime::BlockedRequest;
pub use crate::runtime::BlockedRequestArgs;
@@ -26,35 +25,37 @@ pub use crate::runtime::NetworkProxyState;
pub(crate) use crate::runtime::network_proxy_state_for_policy;
pub(crate) async fn build_config_state() -> Result<ConfigState> {
// Load config through `codex-core` so we inherit the same layer ordering and semantics as the
// rest of Codex (system/managed layers, user layers, session flags, etc.).
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
let cli_overrides = Vec::new();
let overrides = LoaderOverrides::default();
let config_layer_stack = load_config_layers_state(
&codex_home,
None,
&cli_overrides,
overrides,
CloudRequirementsLoader::default(),
)
.await
.context("failed to load Codex config")?;
let cfg_path = codex_home.join(CONFIG_TOML_FILE);
// Deserialize from the merged effective config, rather than parsing config.toml ourselves.
// This avoids a second parser/merger implementation (and the drift that comes with it).
let merged_toml = config_layer_stack.effective_config();
let system_cfg_path = system_config_path();
let system_config = read_toml_file_best_effort(&system_cfg_path)
.await
.context("failed to read system config")?;
let user_config = read_toml_file_best_effort(&cfg_path)
.await
.context("failed to read user config")?;
let mut merged_toml = TomlValue::Table(toml::map::Map::new());
if let Some(system_config) = system_config.clone() {
merge_toml_values(&mut merged_toml, system_config);
}
if let Some(user_config) = user_config {
merge_toml_values(&mut merged_toml, user_config);
}
let config: NetworkProxyConfig = merged_toml
.try_into()
.context("failed to deserialize network proxy config")?;
// Security boundary: user-controlled layers must not be able to widen restrictions set by
// trusted/managed layers (e.g., MDM). Enforce this before building runtime state.
let constraints = enforce_trusted_constraints(&config_layer_stack, &config)?;
let constraints = enforce_trusted_constraints(system_config.as_ref(), &config)?;
let layer_mtimes = collect_layer_mtimes(&config_layer_stack);
let layer_mtimes = vec![
LayerMtime::new(system_cfg_path),
LayerMtime::new(cfg_path.clone()),
];
let deny_set = compile_globset(&config.network.denied_domains)?;
let allow_set = compile_globset(&config.network.allowed_domains)?;
Ok(ConfigState {
@@ -68,26 +69,57 @@ pub(crate) async fn build_config_state() -> Result<ConfigState> {
})
}
fn collect_layer_mtimes(stack: &ConfigLayerStack) -> Vec<LayerMtime> {
stack
.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, false)
.iter()
.filter_map(|layer| {
let path = match &layer.name {
ConfigLayerSource::System { file } => Some(file.as_path().to_path_buf()),
ConfigLayerSource::User { file } => Some(file.as_path().to_path_buf()),
ConfigLayerSource::Project { dot_codex_folder } => dot_codex_folder
.join(CONFIG_TOML_FILE)
.ok()
.map(|p| p.as_path().to_path_buf()),
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => {
Some(file.as_path().to_path_buf())
fn system_config_path() -> std::path::PathBuf {
#[cfg(unix)]
{
Path::new(SYSTEM_CONFIG_TOML_FILE_UNIX).to_path_buf()
}
#[cfg(not(unix))]
{
// Use a dummy path on non-Unix platforms. This keeps the reload logic stable without
// needing per-platform config stack logic yet.
std::path::PathBuf::from("__no_system_config.toml")
}
}
async fn read_toml_file_best_effort(path: &Path) -> Result<Option<TomlValue>> {
let contents = match fs::read_to_string(path).await {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
tracing::warn!(
path = %path.display(),
error = %err,
"permission denied reading config; ignoring"
);
return Ok(None);
}
Err(err) => return Err(err).context(format!("read {}", path.display())),
};
let parsed: TomlValue = contents
.parse()
.with_context(|| format!("parse {}", path.display()))?;
Ok(Some(parsed))
}
fn merge_toml_values(base: &mut TomlValue, overlay: TomlValue) {
match (base, overlay) {
(TomlValue::Table(base_table), TomlValue::Table(overlay_table)) => {
for (key, value) in overlay_table {
match base_table.get_mut(&key) {
Some(existing) => merge_toml_values(existing, value),
None => {
base_table.insert(key, value);
}
}
_ => None,
};
path.map(LayerMtime::new)
})
.collect()
}
}
(base_slot, overlay_value) => {
*base_slot = overlay_value;
}
}
}
#[derive(Debug, Default, Deserialize)]
@@ -127,103 +159,83 @@ pub(crate) struct NetworkProxyConstraints {
}
fn enforce_trusted_constraints(
layers: &codex_core::config_loader::ConfigLayerStack,
system_config: Option<&TomlValue>,
config: &NetworkProxyConfig,
) -> Result<NetworkProxyConstraints> {
let constraints = network_constraints_from_trusted_layers(layers)?;
let constraints = network_constraints_from_system_config(system_config)?;
validate_policy_against_constraints(config, &constraints)
.context("network proxy constraints")?;
Ok(constraints)
}
fn network_constraints_from_trusted_layers(
layers: &codex_core::config_loader::ConfigLayerStack,
fn network_constraints_from_system_config(
system_config: Option<&TomlValue>,
) -> Result<NetworkProxyConstraints> {
let mut constraints = NetworkProxyConstraints::default();
for layer in layers.get_layers(
codex_core::config_loader::ConfigLayerStackOrdering::LowestPrecedenceFirst,
false,
) {
// Only trusted layers contribute constraints. User-controlled layers can narrow policy but
// must never widen beyond what managed config allows.
if is_user_controlled_layer(&layer.name) {
continue;
}
let Some(system_config) = system_config else {
return Ok(constraints);
};
let partial: PartialConfig = layer
.config
.clone()
.try_into()
.context("failed to deserialize trusted config layer")?;
let partial: PartialConfig = system_config
.clone()
.try_into()
.context("failed to deserialize system config constraints")?;
if let Some(enabled) = partial.network.enabled {
constraints.enabled = Some(enabled);
}
if let Some(mode) = partial.network.mode {
constraints.mode = Some(mode);
}
if let Some(allow_upstream_proxy) = partial.network.allow_upstream_proxy {
constraints.allow_upstream_proxy = Some(allow_upstream_proxy);
}
if let Some(dangerously_allow_non_loopback_proxy) =
partial.network.dangerously_allow_non_loopback_proxy
{
constraints.dangerously_allow_non_loopback_proxy =
Some(dangerously_allow_non_loopback_proxy);
}
if let Some(dangerously_allow_non_loopback_admin) =
partial.network.dangerously_allow_non_loopback_admin
{
constraints.dangerously_allow_non_loopback_admin =
Some(dangerously_allow_non_loopback_admin);
}
if let Some(allowed_domains) = partial.network.allowed_domains {
constraints.allowed_domains = Some(allowed_domains);
}
if let Some(denied_domains) = partial.network.denied_domains {
constraints.denied_domains = Some(denied_domains);
}
if let Some(allow_unix_sockets) = partial.network.allow_unix_sockets {
constraints.allow_unix_sockets = Some(allow_unix_sockets);
}
if let Some(allow_local_binding) = partial.network.allow_local_binding {
constraints.allow_local_binding = Some(allow_local_binding);
}
if let Some(enabled) = partial.network.enabled {
constraints.enabled = Some(enabled);
}
if let Some(mode) = partial.network.mode {
constraints.mode = Some(mode);
}
if let Some(allow_upstream_proxy) = partial.network.allow_upstream_proxy {
constraints.allow_upstream_proxy = Some(allow_upstream_proxy);
}
if let Some(dangerously_allow_non_loopback_proxy) =
partial.network.dangerously_allow_non_loopback_proxy
{
constraints.dangerously_allow_non_loopback_proxy =
Some(dangerously_allow_non_loopback_proxy);
}
if let Some(dangerously_allow_non_loopback_admin) =
partial.network.dangerously_allow_non_loopback_admin
{
constraints.dangerously_allow_non_loopback_admin =
Some(dangerously_allow_non_loopback_admin);
}
Ok(constraints)
}
fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool {
matches!(
layer,
ConfigLayerSource::User { .. }
| ConfigLayerSource::Project { .. }
| ConfigLayerSource::SessionFlags
)
if let Some(allowed_domains) = partial.network.allowed_domains {
constraints.allowed_domains = Some(allowed_domains);
}
if let Some(denied_domains) = partial.network.denied_domains {
constraints.denied_domains = Some(denied_domains);
}
if let Some(allow_unix_sockets) = partial.network.allow_unix_sockets {
constraints.allow_unix_sockets = Some(allow_unix_sockets);
}
if let Some(allow_local_binding) = partial.network.allow_local_binding {
constraints.allow_local_binding = Some(allow_local_binding);
}
Ok(constraints)
}
pub(crate) fn validate_policy_against_constraints(
config: &NetworkProxyConfig,
constraints: &NetworkProxyConstraints,
) -> std::result::Result<(), ConstraintError> {
) -> Result<()> {
fn invalid_value(
field_name: &'static str,
candidate: impl Into<String>,
allowed: impl Into<String>,
) -> ConstraintError {
ConstraintError::InvalidValue {
field_name,
candidate: candidate.into(),
allowed: allowed.into(),
requirement_source: RequirementSource::Unknown,
}
) -> anyhow::Error {
anyhow::anyhow!(
"invalid value for {field_name}: candidate={} allowed={}",
candidate.into(),
allowed.into()
)
}
fn validate<T>(
candidate: T,
validator: impl FnOnce(&T) -> std::result::Result<(), ConstraintError>,
) -> std::result::Result<(), ConstraintError> {
fn validate<T>(candidate: T, validator: impl FnOnce(&T) -> Result<()>) -> Result<()> {
validator(&candidate)
}