mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Remove the unused network proxy loader (#33446)
## What changed - Remove the standalone network proxy config loader, its mtime-based reloader, and their tests. - Remove the helper exports and MITM action-reference validator used only by that loader. GitOrigin-RevId: 60895fb4e3461e8c3f19db70869050410a4f5f07
This commit is contained in:
@@ -453,29 +453,6 @@ impl NetworkMitmToml {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_action_references(
|
||||
&self,
|
||||
actions_by_name: &IndexMap<String, NetworkMitmActionToml>,
|
||||
) -> Result<(), String> {
|
||||
self.validate_action_definitions()?;
|
||||
|
||||
let Some(hooks) = self.hooks.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for (hook_name, hook) in hooks {
|
||||
for action_name in &hook.action {
|
||||
if !actions_by_name.contains_key(action_name) {
|
||||
return Err(format!(
|
||||
"network.mitm.hooks.{hook_name}.action references undefined action `{action_name}`"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_runtime_hooks(
|
||||
&self,
|
||||
actions_by_name: Option<&IndexMap<String, NetworkMitmActionToml>>,
|
||||
|
||||
@@ -173,8 +173,6 @@ use permission_profile_catalog::permission_profile_catalog_from_permissions;
|
||||
use permission_profile_catalog::permission_profile_is_allowed;
|
||||
use permission_profile_catalog::validate_permission_profile_for_deny_read;
|
||||
pub(crate) use permissions::is_builtin_permission_profile_name;
|
||||
pub(crate) use permissions::reject_unknown_builtin_permission_profile;
|
||||
pub(crate) use permissions::resolve_permission_profile;
|
||||
pub use resolved_permission_profile::PermissionProfileSnapshot;
|
||||
pub(crate) use resolved_permission_profile::PermissionProfileState;
|
||||
|
||||
|
||||
@@ -59,11 +59,7 @@ mod mcp_skill_dependencies;
|
||||
mod mcp_tool_approval_templates;
|
||||
mod mcp_tool_exposure;
|
||||
mod network_policy_decision;
|
||||
pub(crate) mod network_proxy_loader;
|
||||
pub use mcp::McpManager;
|
||||
pub use network_proxy_loader::MtimeConfigReloader;
|
||||
pub use network_proxy_loader::build_network_proxy_state;
|
||||
pub use network_proxy_loader::build_network_proxy_state_and_reloader;
|
||||
mod original_image_detail;
|
||||
pub use codex_mcp::CodexAppsToolsCache;
|
||||
pub use codex_mcp::SandboxState;
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
use crate::config::find_codex_home;
|
||||
use crate::config::is_builtin_permission_profile_name;
|
||||
use crate::config::reject_unknown_builtin_permission_profile;
|
||||
use crate::config::resolve_permission_profile;
|
||||
use crate::exec_policy::format_exec_policy_error_with_source;
|
||||
use crate::exec_policy::load_exec_policy_with_warning;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_config::merge_toml_values;
|
||||
use codex_config::permissions_toml::NetworkMitmActionToml;
|
||||
use codex_config::permissions_toml::NetworkMitmHookToml;
|
||||
use codex_config::permissions_toml::NetworkMitmToml;
|
||||
use codex_config::permissions_toml::NetworkToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::permissions_toml::overlay_network_domain_permissions;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_network_proxy::ConfigReloader;
|
||||
use codex_network_proxy::ConfigReloaderFuture;
|
||||
use codex_network_proxy::ConfigState;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkProxyConstraintError;
|
||||
use codex_network_proxy::NetworkProxyConstraints;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use codex_network_proxy::build_config_state;
|
||||
use codex_network_proxy::normalize_host;
|
||||
use codex_network_proxy::validate_policy_against_constraints;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub async fn build_network_proxy_state() -> Result<NetworkProxyState> {
|
||||
let (state, reloader) = build_network_proxy_state_and_reloader().await?;
|
||||
Ok(NetworkProxyState::with_reloader(state, Arc::new(reloader)))
|
||||
}
|
||||
|
||||
pub async fn build_network_proxy_state_and_reloader() -> Result<(ConfigState, MtimeConfigReloader)>
|
||||
{
|
||||
let (state, layer_mtimes) = build_config_state_with_mtimes().await?;
|
||||
Ok((state, MtimeConfigReloader::new(layer_mtimes)))
|
||||
}
|
||||
|
||||
async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec<LayerMtime>)> {
|
||||
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(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
/*cwd*/ None,
|
||||
&cli_overrides,
|
||||
overrides,
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.context("failed to load Codex config")?;
|
||||
|
||||
let layer_mtimes = collect_layer_mtimes(&config_layer_stack);
|
||||
let state = build_config_state_from_layers(&config_layer_stack).await?;
|
||||
Ok((state, layer_mtimes))
|
||||
}
|
||||
|
||||
async fn build_config_state_from_layers(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> Result<ConfigState> {
|
||||
let (exec_policy, warning) = load_exec_policy_with_warning(config_layer_stack).await?;
|
||||
if let Some(err) = warning.as_ref() {
|
||||
tracing::warn!(
|
||||
"failed to parse execpolicy while building network proxy state: {}",
|
||||
format_exec_policy_error_with_source(err)
|
||||
);
|
||||
}
|
||||
|
||||
let config = config_from_layers(config_layer_stack, &exec_policy)?;
|
||||
|
||||
let constraints = enforce_trusted_constraints(config_layer_stack, &config)?;
|
||||
build_config_state(config, constraints)
|
||||
}
|
||||
|
||||
fn collect_layer_mtimes(stack: &ConfigLayerStack) -> Vec<LayerMtime> {
|
||||
stack
|
||||
.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
)
|
||||
.iter()
|
||||
.filter_map(|layer| {
|
||||
let path = match &layer.name {
|
||||
ConfigLayerSource::System { file } => Some(file.clone()),
|
||||
ConfigLayerSource::User { file, .. } => Some(file.clone()),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => {
|
||||
Some(dot_codex_folder.join(CONFIG_TOML_FILE))
|
||||
}
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.clone()),
|
||||
_ => None,
|
||||
};
|
||||
path.map(LayerMtime::new)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn enforce_trusted_constraints(
|
||||
layers: &ConfigLayerStack,
|
||||
config: &NetworkProxyConfig,
|
||||
) -> Result<NetworkProxyConstraints> {
|
||||
let constraints = network_constraints_from_trusted_layers(layers)?;
|
||||
validate_policy_against_constraints(config, &constraints)
|
||||
.map_err(NetworkProxyConstraintError::into_anyhow)
|
||||
.context("network proxy constraints")?;
|
||||
Ok(constraints)
|
||||
}
|
||||
|
||||
fn network_constraints_from_trusted_layers(
|
||||
layers: &ConfigLayerStack,
|
||||
) -> Result<NetworkProxyConstraints> {
|
||||
let mut constraints = NetworkProxyConstraints::default();
|
||||
let mut merged = toml::Value::Table(toml::map::Map::new());
|
||||
for layer in layers.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
) {
|
||||
if is_user_controlled_layer(&layer.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
merge_toml_values(&mut merged, &layer.config);
|
||||
}
|
||||
|
||||
let parsed = network_tables_from_toml(&merged)?;
|
||||
if let Some(network) = selected_network_from_tables(parsed)? {
|
||||
apply_network_constraints(network, &mut constraints);
|
||||
}
|
||||
Ok(constraints)
|
||||
}
|
||||
|
||||
fn apply_network_constraints(network: NetworkToml, constraints: &mut NetworkProxyConstraints) {
|
||||
if let Some(enabled) = network.enabled {
|
||||
constraints.enabled = Some(enabled);
|
||||
}
|
||||
if let Some(mode) = network.mode {
|
||||
constraints.mode = Some(mode);
|
||||
}
|
||||
if let Some(allow_upstream_proxy) = network.allow_upstream_proxy {
|
||||
constraints.allow_upstream_proxy = Some(allow_upstream_proxy);
|
||||
}
|
||||
if let Some(dangerously_allow_non_loopback_proxy) = network.dangerously_allow_non_loopback_proxy
|
||||
{
|
||||
constraints.dangerously_allow_non_loopback_proxy =
|
||||
Some(dangerously_allow_non_loopback_proxy);
|
||||
}
|
||||
if let Some(dangerously_allow_all_unix_sockets) = network.dangerously_allow_all_unix_sockets {
|
||||
constraints.dangerously_allow_all_unix_sockets = Some(dangerously_allow_all_unix_sockets);
|
||||
}
|
||||
if let Some(domains) = network.domains.as_ref() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
if let Some(allowed_domains) = constraints.allowed_domains.take() {
|
||||
config.set_allowed_domains(allowed_domains);
|
||||
}
|
||||
if let Some(denied_domains) = constraints.denied_domains.take() {
|
||||
config.set_denied_domains(denied_domains);
|
||||
}
|
||||
overlay_network_domain_permissions(&mut config, domains);
|
||||
constraints.allowed_domains = config.allowed_domains();
|
||||
constraints.denied_domains = config.denied_domains();
|
||||
}
|
||||
if let Some(unix_sockets) = network.unix_sockets.as_ref() {
|
||||
let allow_unix_sockets = unix_sockets.allow_unix_sockets();
|
||||
constraints.allow_unix_sockets = Some(allow_unix_sockets);
|
||||
}
|
||||
if let Some(allow_local_binding) = network.allow_local_binding {
|
||||
constraints.allow_local_binding = Some(allow_local_binding);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
struct NetworkTablesToml {
|
||||
default_permissions: Option<String>,
|
||||
permissions: Option<PermissionsToml>,
|
||||
}
|
||||
|
||||
fn network_tables_from_toml(value: &toml::Value) -> Result<NetworkTablesToml> {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.context("failed to deserialize network tables from config")
|
||||
}
|
||||
|
||||
fn selected_network_from_tables(parsed: NetworkTablesToml) -> Result<Option<NetworkToml>> {
|
||||
let Some(default_permissions) = parsed.default_permissions else {
|
||||
return Ok(None);
|
||||
};
|
||||
if is_builtin_permission_profile_name(&default_permissions) {
|
||||
return Ok(None);
|
||||
}
|
||||
reject_unknown_builtin_permission_profile(&default_permissions)?;
|
||||
|
||||
let permissions = parsed
|
||||
.permissions
|
||||
.context("default_permissions requires a `[permissions]` table for network settings")?;
|
||||
let profile = resolve_permission_profile(&permissions, &default_permissions)
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(profile.network)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesToml) -> Result<()> {
|
||||
if let Some(network) = selected_network_from_tables(parsed)? {
|
||||
network.apply_to_network_proxy_config(config);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NetworkConfigAccumulator {
|
||||
config: NetworkProxyConfig,
|
||||
mitm_hooks: IndexMap<String, NetworkMitmHookToml>,
|
||||
mitm_actions: IndexMap<String, NetworkMitmActionToml>,
|
||||
}
|
||||
|
||||
impl NetworkConfigAccumulator {
|
||||
fn apply_network_tables(&mut self, parsed: NetworkTablesToml) -> Result<()> {
|
||||
if let Some(network) = selected_network_from_tables(parsed)? {
|
||||
self.apply_network(network);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_network(&mut self, mut network: NetworkToml) {
|
||||
let mitm = network.mitm.take();
|
||||
network.apply_to_network_proxy_config(&mut self.config);
|
||||
|
||||
if let Some(mitm) = mitm {
|
||||
if let Some(actions) = mitm.actions {
|
||||
self.mitm_actions.extend(actions);
|
||||
}
|
||||
if let Some(hooks) = mitm.hooks {
|
||||
self.mitm_hooks.extend(hooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self) -> Result<NetworkProxyConfig> {
|
||||
if !self.mitm_hooks.is_empty() {
|
||||
let actions = self.mitm_actions;
|
||||
let mitm = NetworkMitmToml {
|
||||
hooks: Some(self.mitm_hooks),
|
||||
actions: Some(actions.clone()),
|
||||
};
|
||||
mitm.validate_action_references(&actions)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
self.config.mitm_hooks = mitm.to_runtime_hooks(Some(&actions));
|
||||
}
|
||||
|
||||
self.config.mitm =
|
||||
self.config.mode == NetworkMode::Limited || !self.config.mitm_hooks.is_empty();
|
||||
Ok(self.config)
|
||||
}
|
||||
}
|
||||
|
||||
fn config_from_layers(
|
||||
layers: &ConfigLayerStack,
|
||||
exec_policy: &codex_execpolicy::Policy,
|
||||
) -> Result<NetworkProxyConfig> {
|
||||
let mut merged = toml::Value::Table(toml::map::Map::new());
|
||||
for layer in layers.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
) {
|
||||
merge_toml_values(&mut merged, &layer.config);
|
||||
}
|
||||
let parsed = network_tables_from_toml(&merged)?;
|
||||
let mut accumulator = NetworkConfigAccumulator::default();
|
||||
accumulator.apply_network_tables(parsed)?;
|
||||
let mut config = accumulator.finish()?;
|
||||
apply_exec_policy_network_rules(&mut config, exec_policy);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn apply_exec_policy_network_rules(
|
||||
config: &mut NetworkProxyConfig,
|
||||
exec_policy: &codex_execpolicy::Policy,
|
||||
) {
|
||||
let (allowed_domains, denied_domains) = exec_policy.compiled_network_domains();
|
||||
for host in allowed_domains {
|
||||
upsert_network_domain(
|
||||
config,
|
||||
host,
|
||||
codex_network_proxy::NetworkDomainPermission::Allow,
|
||||
);
|
||||
}
|
||||
for host in denied_domains {
|
||||
upsert_network_domain(
|
||||
config,
|
||||
host,
|
||||
codex_network_proxy::NetworkDomainPermission::Deny,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn upsert_network_domain(
|
||||
config: &mut NetworkProxyConfig,
|
||||
host: String,
|
||||
permission: codex_network_proxy::NetworkDomainPermission,
|
||||
) {
|
||||
config.upsert_domain_permission(host, permission, normalize_host);
|
||||
}
|
||||
|
||||
fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool {
|
||||
matches!(
|
||||
layer,
|
||||
ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
| ConfigLayerSource::SessionFlags
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LayerMtime {
|
||||
path: AbsolutePathBuf,
|
||||
mtime: Option<std::time::SystemTime>,
|
||||
}
|
||||
|
||||
impl LayerMtime {
|
||||
fn new(path: AbsolutePathBuf) -> Self {
|
||||
let mtime = path.metadata().and_then(|m| m.modified()).ok();
|
||||
Self { path, mtime }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MtimeConfigReloader {
|
||||
layer_mtimes: RwLock<Vec<LayerMtime>>,
|
||||
}
|
||||
|
||||
impl MtimeConfigReloader {
|
||||
fn new(layer_mtimes: Vec<LayerMtime>) -> Self {
|
||||
Self {
|
||||
layer_mtimes: RwLock::new(layer_mtimes),
|
||||
}
|
||||
}
|
||||
|
||||
async fn needs_reload(&self) -> bool {
|
||||
let guard = self.layer_mtimes.read().await;
|
||||
guard.iter().any(|layer| {
|
||||
let metadata = std::fs::metadata(&layer.path).ok();
|
||||
match (metadata.and_then(|m| m.modified().ok()), layer.mtime) {
|
||||
(Some(new_mtime), Some(old_mtime)) => new_mtime > old_mtime,
|
||||
(Some(_), None) => true,
|
||||
(None, Some(_)) => true,
|
||||
(None, None) => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn maybe_reload(&self) -> Result<Option<ConfigState>> {
|
||||
if !self.needs_reload().await {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (state, layer_mtimes) = build_config_state_with_mtimes().await?;
|
||||
let mut guard = self.layer_mtimes.write().await;
|
||||
*guard = layer_mtimes;
|
||||
Ok(Some(state))
|
||||
}
|
||||
|
||||
async fn reload_now(&self) -> Result<ConfigState> {
|
||||
let (state, layer_mtimes) = build_config_state_with_mtimes().await?;
|
||||
let mut guard = self.layer_mtimes.write().await;
|
||||
*guard = layer_mtimes;
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigReloader for MtimeConfigReloader {
|
||||
fn source_label(&self) -> String {
|
||||
"config layers".to_string()
|
||||
}
|
||||
|
||||
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
|
||||
Box::pin(MtimeConfigReloader::maybe_reload(self))
|
||||
}
|
||||
|
||||
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
|
||||
Box::pin(MtimeConfigReloader::reload_now(self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "network_proxy_loader_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,704 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::RequirementsExecPolicy;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::permissions_toml::NetworkDomainPermissionToml;
|
||||
use codex_config::permissions_toml::NetworkDomainPermissionsToml;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::NetworkRuleProtocol;
|
||||
use codex_execpolicy::Policy;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_profile_network_overlays_domain_entries() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network]
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"lower.example.com" = "allow"
|
||||
"blocked.example.com" = "deny"
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network]
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"higher.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
)
|
||||
.expect("lower layer should apply");
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
network_tables_from_toml(&higher_network).expect("higher layer should deserialize"),
|
||||
)
|
||||
.expect("higher layer should apply");
|
||||
|
||||
assert_eq!(
|
||||
config.allowed_domains(),
|
||||
Some(vec![
|
||||
"lower.example.com".to_string(),
|
||||
"higher.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
config.denied_domains(),
|
||||
Some(vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_profile_network_overrides_matching_domain_entries() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network]
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"shared.example.com" = "deny"
|
||||
"other.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network]
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"shared.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
)
|
||||
.expect("lower layer should apply");
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
network_tables_from_toml(&higher_network).expect("higher layer should deserialize"),
|
||||
)
|
||||
.expect("higher layer should apply");
|
||||
|
||||
assert_eq!(
|
||||
config.allowed_domains(),
|
||||
Some(vec![
|
||||
"other.example.com".to_string(),
|
||||
"shared.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(config.denied_domains(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_profile_network_overrides_named_mitm_actions() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
mode = "full"
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"lower.example.com" = "allow"
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["strip_auth"]
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["authorization"]
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
mode = "full"
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"higher.example.com" = "allow"
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["x-api-key"]
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
|
||||
let mut accumulator = NetworkConfigAccumulator::default();
|
||||
accumulator
|
||||
.apply_network_tables(
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
)
|
||||
.expect("lower layer should apply");
|
||||
accumulator
|
||||
.apply_network_tables(
|
||||
network_tables_from_toml(&higher_network).expect("higher layer should deserialize"),
|
||||
)
|
||||
.expect("higher layer should apply");
|
||||
let config = accumulator.finish().expect("merged config should build");
|
||||
|
||||
assert_eq!(config.mode, codex_network_proxy::NetworkMode::Full);
|
||||
assert!(config.mitm);
|
||||
assert_eq!(
|
||||
config.allowed_domains(),
|
||||
Some(vec![
|
||||
"lower.example.com".to_string(),
|
||||
"higher.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(config.mitm_hooks.len(), 1);
|
||||
assert_eq!(config.mitm_hooks[0].host, "api.github.com");
|
||||
assert_eq!(config.mitm_hooks[0].matcher.methods, vec!["POST"]);
|
||||
assert_eq!(
|
||||
config.mitm_hooks[0].actions.strip_request_headers,
|
||||
vec!["x-api-key"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execpolicy_network_rules_overlay_network_lists() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.set_allowed_domains(vec!["config.example.com".to_string()]);
|
||||
config.set_denied_domains(vec!["blocked.example.com".to_string()]);
|
||||
|
||||
let mut exec_policy = Policy::empty();
|
||||
exec_policy
|
||||
.add_network_rule(
|
||||
"blocked.example.com",
|
||||
NetworkRuleProtocol::Https,
|
||||
Decision::Allow,
|
||||
/*justification*/ None,
|
||||
)
|
||||
.expect("allow rule should be valid");
|
||||
exec_policy
|
||||
.add_network_rule(
|
||||
"api.example.com",
|
||||
NetworkRuleProtocol::Http,
|
||||
Decision::Forbidden,
|
||||
/*justification*/ None,
|
||||
)
|
||||
.expect("deny rule should be valid");
|
||||
|
||||
apply_exec_policy_network_rules(&mut config, &exec_policy);
|
||||
|
||||
assert_eq!(
|
||||
config.allowed_domains(),
|
||||
Some(vec![
|
||||
"config.example.com".to_string(),
|
||||
"blocked.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
config.denied_domains(),
|
||||
Some(vec!["api.example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_custom_rules_preserve_managed_denied_domain() {
|
||||
let temp_dir = tempdir().expect("create temp dir");
|
||||
let policy_dir = temp_dir.path().join("rules");
|
||||
fs::create_dir_all(&policy_dir).expect("create policy dir");
|
||||
fs::write(policy_dir.join("broken.rules"), "prefix_rule(").expect("write malformed policy");
|
||||
|
||||
let mut requirements_exec_policy = Policy::empty();
|
||||
requirements_exec_policy
|
||||
.add_network_rule(
|
||||
"blocked.example.com",
|
||||
NetworkRuleProtocol::Https,
|
||||
Decision::Forbidden,
|
||||
/*justification*/ None,
|
||||
)
|
||||
.expect("managed network rule should be valid");
|
||||
let requirements = ConfigRequirements {
|
||||
exec_policy: Some(Sourced::new(
|
||||
RequirementsExecPolicy::new(requirements_exec_policy),
|
||||
RequirementSource::Unknown,
|
||||
)),
|
||||
..ConfigRequirements::default()
|
||||
};
|
||||
let dot_codex_folder = AbsolutePathBuf::from_absolute_path(temp_dir.path())
|
||||
.expect("dot codex folder should be absolute");
|
||||
let layers = ConfigLayerStack::new(
|
||||
vec![ConfigLayerEntry::new(
|
||||
ConfigLayerSource::Project { dot_codex_folder },
|
||||
toml::Value::Table(Default::default()),
|
||||
)],
|
||||
requirements,
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("layer stack should be valid");
|
||||
|
||||
let state = build_config_state_from_layers(&layers)
|
||||
.await
|
||||
.expect("proxy state should tolerate malformed custom rules");
|
||||
|
||||
assert_eq!(
|
||||
state.config.denied_domains(),
|
||||
Some(vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_network_constraints_includes_allow_all_unix_sockets_flag() {
|
||||
let config: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network]
|
||||
dangerously_allow_all_unix_sockets = true
|
||||
"#,
|
||||
)
|
||||
.expect("permissions profile should parse");
|
||||
let network = selected_network_from_tables(
|
||||
network_tables_from_toml(&config).expect("permissions profile should deserialize"),
|
||||
)
|
||||
.expect("permissions profile should select a network table")
|
||||
.expect("network table should be present");
|
||||
|
||||
let mut constraints = NetworkProxyConstraints::default();
|
||||
apply_network_constraints(network, &mut constraints);
|
||||
|
||||
assert_eq!(constraints.dangerously_allow_all_unix_sockets, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_network_from_tables_ignores_builtin_profile_without_permissions_table() {
|
||||
let config: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = ":workspace"
|
||||
"#,
|
||||
)
|
||||
.expect("built-in profile config should parse");
|
||||
|
||||
let network = selected_network_from_tables(
|
||||
network_tables_from_toml(&config).expect("built-in profile config should deserialize"),
|
||||
)
|
||||
.expect("built-in profile selection should not require permissions tables");
|
||||
|
||||
assert_eq!(network, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_network_from_tables_rejects_unknown_builtin_profile_without_permissions_table() {
|
||||
let config: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = ":unknown"
|
||||
"#,
|
||||
)
|
||||
.expect("unknown built-in config should parse");
|
||||
|
||||
let err = selected_network_from_tables(
|
||||
network_tables_from_toml(&config).expect("unknown built-in config should deserialize"),
|
||||
)
|
||||
.expect_err("unknown built-in profile should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"default_permissions refers to unknown built-in profile `:unknown`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_network_from_tables_resolves_builtin_workspace_parent() {
|
||||
let config: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev]
|
||||
extends = ":workspace"
|
||||
|
||||
[permissions.dev.network]
|
||||
enabled = true
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"child.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("dev extension config should parse");
|
||||
|
||||
let network = selected_network_from_tables(
|
||||
network_tables_from_toml(&config).expect("dev extension config should deserialize"),
|
||||
)
|
||||
.expect("dev extension should resolve")
|
||||
.expect("dev extension should expose child network config");
|
||||
|
||||
assert_eq!(
|
||||
network,
|
||||
NetworkToml {
|
||||
enabled: Some(true),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"child.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_network_from_tables_resolves_permission_profile_inheritance() {
|
||||
let config: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.base.network]
|
||||
enabled = true
|
||||
dangerously_allow_all_unix_sockets = true
|
||||
|
||||
[permissions.base.network.domains]
|
||||
"base.example.com" = "allow"
|
||||
"shared.example.com" = "deny"
|
||||
|
||||
[permissions.dev]
|
||||
extends = "base"
|
||||
|
||||
[permissions.dev.network]
|
||||
allow_local_binding = true
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"child.example.com" = "allow"
|
||||
"shared.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("permissions profiles should parse");
|
||||
|
||||
let network = selected_network_from_tables(
|
||||
network_tables_from_toml(&config).expect("permissions profiles should deserialize"),
|
||||
)
|
||||
.expect("permissions profiles should select a network table")
|
||||
.expect("network table should be present");
|
||||
|
||||
assert_eq!(
|
||||
network,
|
||||
NetworkToml {
|
||||
enabled: Some(true),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
allow_local_binding: Some(true),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
"base.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"child.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"shared.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_from_layers_resolves_inherited_profiles_across_layers() {
|
||||
let lower_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::toml! {
|
||||
[permissions.base.network.domains]
|
||||
"base.example.com" = "allow"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let higher_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::toml! {
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev]
|
||||
extends = "base"
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"child.example.com" = "allow"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let layers = ConfigLayerStack::new(
|
||||
vec![lower_layer, higher_layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("layer stack should be valid");
|
||||
|
||||
let config =
|
||||
config_from_layers(&layers, &Policy::empty()).expect("inherited profiles should load");
|
||||
|
||||
assert_eq!(
|
||||
config.allowed_domains(),
|
||||
Some(vec![
|
||||
"base.example.com".to_string(),
|
||||
"child.example.com".to_string(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_from_layers_normalizes_profile_network_domains_before_merging_layers() {
|
||||
let lower_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::toml! {
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"example.com" = "deny"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let higher_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::toml! {
|
||||
[permissions.dev.network.domains]
|
||||
"EXAMPLE.COM" = "allow"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let layers = ConfigLayerStack::new(
|
||||
vec![lower_layer, higher_layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("layer stack should be valid");
|
||||
|
||||
let config = config_from_layers(&layers, &Policy::empty())
|
||||
.expect("network domain layer precedence should load");
|
||||
|
||||
assert_eq!(
|
||||
config.allowed_domains(),
|
||||
Some(vec!["example.com".to_string()])
|
||||
);
|
||||
assert_eq!(config.denied_domains(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_from_layers_uses_only_the_final_selected_profile_network() {
|
||||
let lower_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::toml! {
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"lower.example.com" = "allow"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let higher_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::toml! {
|
||||
default_permissions = ":workspace"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let layers = ConfigLayerStack::new(
|
||||
vec![lower_layer, higher_layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("layer stack should be valid");
|
||||
|
||||
let config = config_from_layers(&layers, &Policy::empty())
|
||||
.expect("final built-in profile selection should load");
|
||||
|
||||
assert_eq!(config.allowed_domains(), None);
|
||||
assert_eq!(config.denied_domains(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_constraints_use_only_the_final_selected_profile_network() {
|
||||
let lower_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::System {
|
||||
file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/system.toml"))
|
||||
.expect("system config path should be absolute"),
|
||||
},
|
||||
toml::toml! {
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"managed.example.com" = "allow"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let higher_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile {
|
||||
file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/managed.toml"))
|
||||
.expect("managed config path should be absolute"),
|
||||
},
|
||||
toml::toml! {
|
||||
default_permissions = ":workspace"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let layers = ConfigLayerStack::new(
|
||||
vec![lower_layer, higher_layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("layer stack should be valid");
|
||||
|
||||
let constraints = network_constraints_from_trusted_layers(&layers)
|
||||
.expect("final built-in trusted selection should load");
|
||||
|
||||
assert_eq!(constraints.allowed_domains, None);
|
||||
assert_eq!(constraints.denied_domains, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_constraints_normalize_profile_network_domains_before_merging_layers() {
|
||||
let lower_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::System {
|
||||
file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/system.toml"))
|
||||
.expect("system config path should be absolute"),
|
||||
},
|
||||
toml::toml! {
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"example.com" = "deny"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let higher_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile {
|
||||
file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/managed.toml"))
|
||||
.expect("managed config path should be absolute"),
|
||||
},
|
||||
toml::toml! {
|
||||
[permissions.dev.network.domains]
|
||||
"EXAMPLE.COM" = "allow"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let layers = ConfigLayerStack::new(
|
||||
vec![lower_layer, higher_layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("layer stack should be valid");
|
||||
|
||||
let constraints = network_constraints_from_trusted_layers(&layers)
|
||||
.expect("trusted network domain layer precedence should load");
|
||||
|
||||
assert_eq!(
|
||||
constraints.allowed_domains,
|
||||
Some(vec!["example.com".to_string()])
|
||||
);
|
||||
assert_eq!(constraints.denied_domains, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_network_constraints_skips_empty_domain_sides() {
|
||||
let config: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network]
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"managed.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("permissions profile should parse");
|
||||
let network = selected_network_from_tables(
|
||||
network_tables_from_toml(&config).expect("permissions profile should deserialize"),
|
||||
)
|
||||
.expect("permissions profile should select a network table")
|
||||
.expect("network table should be present");
|
||||
|
||||
let mut constraints = NetworkProxyConstraints::default();
|
||||
apply_network_constraints(network, &mut constraints);
|
||||
|
||||
assert_eq!(
|
||||
constraints.allowed_domains,
|
||||
Some(vec!["managed.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(constraints.denied_domains, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_network_constraints_overlay_domain_entries() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network]
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"blocked.example.com" = "deny"
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.network]
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"api.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
|
||||
let lower_network = selected_network_from_tables(
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
)
|
||||
.expect("lower layer should select a network table")
|
||||
.expect("lower network table should be present");
|
||||
let higher_network = selected_network_from_tables(
|
||||
network_tables_from_toml(&higher_network).expect("higher layer should deserialize"),
|
||||
)
|
||||
.expect("higher layer should select a network table")
|
||||
.expect("higher network table should be present");
|
||||
|
||||
let mut constraints = NetworkProxyConstraints::default();
|
||||
apply_network_constraints(lower_network, &mut constraints);
|
||||
apply_network_constraints(higher_network, &mut constraints);
|
||||
|
||||
assert_eq!(
|
||||
constraints.allowed_domains,
|
||||
Some(vec!["api.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
constraints.denied_domains,
|
||||
Some(vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user