chore: reverse the codex-network-proxy -> codex-core dependency (#11121)

This commit is contained in:
Michael Bolin
2026-02-08 17:03:24 -08:00
committed by GitHub
parent 45b7763c3f
commit ff74aaae21
14 changed files with 376 additions and 320 deletions

View File

@@ -13,7 +13,8 @@ mod socks5;
mod state;
mod upstream;
use anyhow::Result;
pub use config::NetworkMode;
pub use config::NetworkProxyConfig;
pub use network_policy::NetworkDecision;
pub use network_policy::NetworkPolicyDecider;
pub use network_policy::NetworkPolicyRequest;
@@ -23,9 +24,12 @@ pub use proxy::Args;
pub use proxy::NetworkProxy;
pub use proxy::NetworkProxyBuilder;
pub use proxy::NetworkProxyHandle;
pub async fn run_main(args: Args) -> Result<()> {
let _ = args;
let proxy = NetworkProxy::builder().build().await?;
proxy.run().await?.wait().await
}
pub use runtime::ConfigReloader;
pub use runtime::ConfigState;
pub use runtime::NetworkProxyState;
pub use state::NetworkProxyConstraintError;
pub use state::NetworkProxyConstraints;
pub use state::PartialNetworkConfig;
pub use state::PartialNetworkProxyConfig;
pub use state::build_config_state;
pub use state::validate_policy_against_constraints;

View File

@@ -1,14 +0,0 @@
use anyhow::Result;
use clap::Parser;
use codex_network_proxy::Args;
use codex_network_proxy::NetworkProxy;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Args::parse();
let _ = args;
let proxy = NetworkProxy::builder().build().await?;
proxy.run().await?.wait().await
}

View File

@@ -55,10 +55,11 @@ impl NetworkProxyBuilder {
}
pub async fn build(self) -> Result<NetworkProxy> {
let state = match self.state {
Some(state) => state,
None => Arc::new(NetworkProxyState::new().await?),
};
let state = self.state.ok_or_else(|| {
anyhow::anyhow!(
"NetworkProxyBuilder requires a state; supply one via builder.state(...)"
)
})?;
let current_cfg = state.current_cfg().await?;
let runtime = config::resolve_runtime(&current_cfg)?;
// Reapply bind clamping for caller overrides so unix-socket proxying stays loopback-only.

View File

@@ -7,8 +7,10 @@ use crate::policy::normalize_host;
use crate::reasons::REASON_DENIED;
use crate::reasons::REASON_NOT_ALLOWED;
use crate::reasons::REASON_NOT_ALLOWED_LOCAL;
use crate::state::NetworkProxyConstraintError;
use crate::state::NetworkProxyConstraints;
use crate::state::build_default_config_state_and_reloader;
#[cfg(test)]
use crate::state::build_config_state;
use crate::state::validate_policy_against_constraints;
use anyhow::Context;
use anyhow::Result;
@@ -105,17 +107,17 @@ impl BlockedRequest {
}
#[derive(Clone)]
pub(crate) struct ConfigState {
pub(crate) config: NetworkProxyConfig,
pub(crate) allow_set: GlobSet,
pub(crate) deny_set: GlobSet,
pub(crate) constraints: NetworkProxyConstraints,
pub(crate) cfg_path: PathBuf,
pub(crate) blocked: VecDeque<BlockedRequest>,
pub struct ConfigState {
pub config: NetworkProxyConfig,
pub allow_set: GlobSet,
pub deny_set: GlobSet,
pub constraints: NetworkProxyConstraints,
pub cfg_path: PathBuf,
pub blocked: VecDeque<BlockedRequest>,
}
#[async_trait]
pub(crate) trait ConfigReloader: Send + Sync {
pub trait ConfigReloader: Send + Sync {
/// Return a freshly loaded state if a reload is needed; otherwise, return `None`.
async fn maybe_reload(&self) -> Result<Option<ConfigState>>;
@@ -146,12 +148,7 @@ impl Clone for NetworkProxyState {
}
impl NetworkProxyState {
pub async fn new() -> Result<Self> {
let (cfg_state, reloader) = build_default_config_state_and_reloader().await?;
Ok(Self::with_reloader(cfg_state, Arc::new(reloader)))
}
pub(crate) fn with_reloader(state: ConfigState, reloader: Arc<dyn ConfigReloader>) -> Self {
pub fn with_reloader(state: ConfigState, reloader: Arc<dyn ConfigReloader>) -> Self {
Self {
state: Arc::new(RwLock::new(state)),
reloader,
@@ -362,6 +359,7 @@ impl NetworkProxyState {
};
validate_policy_against_constraints(&candidate, &constraints)
.map_err(NetworkProxyConstraintError::into_anyhow)
.context("network.mode constrained by managed config")?;
let mut guard = self.state.write().await;
@@ -495,18 +493,12 @@ pub(crate) fn network_proxy_state_for_policy(
network.enabled = true;
network.mode = NetworkMode::Full;
let config = NetworkProxyConfig { network };
let allow_set = crate::policy::compile_globset(&config.network.allowed_domains).unwrap();
let deny_set = crate::policy::compile_globset(&config.network.denied_domains).unwrap();
let state = ConfigState {
let state = build_config_state(
config,
allow_set,
deny_set,
constraints: NetworkProxyConstraints::default(),
cfg_path: PathBuf::from("/nonexistent/config.toml"),
blocked: VecDeque::new(),
};
NetworkProxyConstraints::default(),
PathBuf::from("/nonexistent/config.toml"),
)
.unwrap();
NetworkProxyState::with_reloader(state, Arc::new(NoopReloader))
}

View File

@@ -2,24 +2,10 @@ use crate::config::NetworkMode;
use crate::config::NetworkProxyConfig;
use crate::policy::DomainPattern;
use crate::policy::compile_globset;
use crate::runtime::ConfigReloader;
use crate::runtime::ConfigState;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
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 serde::Deserialize;
use std::collections::HashSet;
use tokio::sync::RwLock;
use std::path::PathBuf;
pub use crate::runtime::BlockedRequest;
pub use crate::runtime::BlockedRequestArgs;
@@ -27,272 +13,79 @@ pub use crate::runtime::NetworkProxyState;
#[cfg(test)]
pub(crate) use crate::runtime::network_proxy_state_for_policy;
pub(crate) async fn build_default_config_state_and_reloader()
-> Result<(ConfigState, MtimeConfigReloader)> {
let (state, layer_mtimes) = build_config_state_with_mtimes().await?;
Ok((state, MtimeConfigReloader::new(layer_mtimes)))
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct NetworkProxyConstraints {
pub enabled: Option<bool>,
pub mode: Option<NetworkMode>,
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_non_loopback_admin: Option<bool>,
pub allowed_domains: Option<Vec<String>>,
pub denied_domains: Option<Vec<String>>,
pub allow_unix_sockets: Option<Vec<String>>,
pub allow_local_binding: Option<bool>,
}
async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec<LayerMtime>)> {
// 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")?;
#[derive(Debug, Clone, Deserialize)]
pub struct PartialNetworkProxyConfig {
#[serde(default)]
pub network: PartialNetworkConfig,
}
let cfg_path = codex_home.join(CONFIG_TOML_FILE);
#[derive(Debug, Default, Clone, Deserialize)]
pub struct PartialNetworkConfig {
pub enabled: Option<bool>,
pub mode: Option<NetworkMode>,
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_non_loopback_admin: Option<bool>,
#[serde(default)]
pub allowed_domains: Option<Vec<String>>,
#[serde(default)]
pub denied_domains: Option<Vec<String>>,
#[serde(default)]
pub allow_unix_sockets: Option<Vec<String>>,
#[serde(default)]
pub allow_local_binding: Option<bool>,
}
// 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 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 layer_mtimes = collect_layer_mtimes(&config_layer_stack);
pub fn build_config_state(
config: NetworkProxyConfig,
constraints: NetworkProxyConstraints,
cfg_path: PathBuf,
) -> anyhow::Result<ConfigState> {
let deny_set = compile_globset(&config.network.denied_domains)?;
let allow_set = compile_globset(&config.network.allowed_domains)?;
Ok((
ConfigState {
config,
allow_set,
deny_set,
constraints,
cfg_path,
blocked: std::collections::VecDeque::new(),
},
layer_mtimes,
))
Ok(ConfigState {
config,
allow_set,
deny_set,
constraints,
cfg_path,
blocked: std::collections::VecDeque::new(),
})
}
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())
}
_ => None,
};
path.map(LayerMtime::new)
})
.collect()
}
#[derive(Clone)]
struct LayerMtime {
path: std::path::PathBuf,
mtime: Option<std::time::SystemTime>,
}
impl LayerMtime {
fn new(path: std::path::PathBuf) -> Self {
let mtime = path.metadata().and_then(|m| m.modified()).ok();
Self { path, mtime }
}
}
pub(crate) 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_trait]
impl ConfigReloader for MtimeConfigReloader {
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)
}
}
#[derive(Debug, Default, Deserialize)]
struct PartialConfig {
#[serde(default)]
network: PartialNetworkConfig,
}
#[derive(Debug, Default, Deserialize)]
struct PartialNetworkConfig {
enabled: Option<bool>,
mode: Option<NetworkMode>,
allow_upstream_proxy: Option<bool>,
dangerously_allow_non_loopback_proxy: Option<bool>,
dangerously_allow_non_loopback_admin: Option<bool>,
#[serde(default)]
allowed_domains: Option<Vec<String>>,
#[serde(default)]
denied_domains: Option<Vec<String>>,
#[serde(default)]
allow_unix_sockets: Option<Vec<String>>,
#[serde(default)]
allow_local_binding: Option<bool>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct NetworkProxyConstraints {
pub(crate) enabled: Option<bool>,
pub(crate) mode: Option<NetworkMode>,
pub(crate) allow_upstream_proxy: Option<bool>,
pub(crate) dangerously_allow_non_loopback_proxy: Option<bool>,
pub(crate) dangerously_allow_non_loopback_admin: Option<bool>,
pub(crate) allowed_domains: Option<Vec<String>>,
pub(crate) denied_domains: Option<Vec<String>>,
pub(crate) allow_unix_sockets: Option<Vec<String>>,
pub(crate) allow_local_binding: Option<bool>,
}
fn enforce_trusted_constraints(
layers: &codex_core::config_loader::ConfigLayerStack,
config: &NetworkProxyConfig,
) -> Result<NetworkProxyConstraints> {
let constraints = network_constraints_from_trusted_layers(layers)?;
validate_policy_against_constraints(config, &constraints)
.context("network proxy constraints")?;
Ok(constraints)
}
fn network_constraints_from_trusted_layers(
layers: &codex_core::config_loader::ConfigLayerStack,
) -> 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 partial: PartialConfig = layer
.config
.clone()
.try_into()
.context("failed to deserialize trusted config layer")?;
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);
}
}
Ok(constraints)
}
fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool {
matches!(
layer,
ConfigLayerSource::User { .. }
| ConfigLayerSource::Project { .. }
| ConfigLayerSource::SessionFlags
)
}
pub(crate) fn validate_policy_against_constraints(
pub fn validate_policy_against_constraints(
config: &NetworkProxyConfig,
constraints: &NetworkProxyConstraints,
) -> std::result::Result<(), ConstraintError> {
) -> Result<(), NetworkProxyConstraintError> {
fn invalid_value(
field_name: &'static str,
candidate: impl Into<String>,
allowed: impl Into<String>,
) -> ConstraintError {
ConstraintError::InvalidValue {
) -> NetworkProxyConstraintError {
NetworkProxyConstraintError::InvalidValue {
field_name,
candidate: candidate.into(),
allowed: allowed.into(),
requirement_source: RequirementSource::Unknown,
}
}
fn validate<T>(
candidate: T,
validator: impl FnOnce(&T) -> std::result::Result<(), ConstraintError>,
) -> std::result::Result<(), ConstraintError> {
validator: impl FnOnce(&T) -> Result<(), NetworkProxyConstraintError>,
) -> Result<(), NetworkProxyConstraintError> {
validator(&candidate)
}
@@ -479,6 +272,22 @@ pub(crate) fn validate_policy_against_constraints(
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum NetworkProxyConstraintError {
#[error("invalid value for {field_name}: {candidate} (allowed {allowed})")]
InvalidValue {
field_name: &'static str,
candidate: String,
allowed: String,
},
}
impl NetworkProxyConstraintError {
pub fn into_anyhow(self) -> anyhow::Error {
anyhow::anyhow!(self)
}
}
fn network_mode_rank(mode: NetworkMode) -> u8 {
match mode {
NetworkMode::Limited => 0,