chore: reverse the codex-network-proxy -> codex-core dependency (#11121)
This commit is contained in:
parent
45b7763c3f
commit
ff74aaae21
14 changed files with 376 additions and 320 deletions
17
codex-rs/Cargo.lock
generated
17
codex-rs/Cargo.lock
generated
|
|
@ -1569,6 +1569,7 @@ dependencies = [
|
|||
"codex-file-search",
|
||||
"codex-git",
|
||||
"codex-keyring-store",
|
||||
"codex-network-proxy",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-rmcp-client",
|
||||
|
|
@ -1905,8 +1906,6 @@ dependencies = [
|
|||
"anyhow",
|
||||
"async-trait",
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-core",
|
||||
"codex-utils-absolute-path",
|
||||
"globset",
|
||||
"pretty_assertions",
|
||||
|
|
@ -1921,13 +1920,25 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-network-proxy-cli"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-core",
|
||||
"codex-network-proxy",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-ollama"
|
||||
version = "0.0.0"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ members = [
|
|||
"login",
|
||||
"mcp-server",
|
||||
"network-proxy",
|
||||
"network-proxy-cli",
|
||||
"ollama",
|
||||
"process-hardening",
|
||||
"protocol",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ codex-rmcp-client = { workspace = true }
|
|||
codex-state = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
codex-network-proxy = { path = "../network-proxy" }
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-utils-readiness = { workspace = true }
|
||||
codex-utils-string = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ pub mod landlock;
|
|||
pub mod mcp;
|
||||
mod mcp_connection_manager;
|
||||
pub mod models_manager;
|
||||
pub mod network_proxy_loader;
|
||||
pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY;
|
||||
pub use mcp_connection_manager::MCP_SANDBOX_STATE_METHOD;
|
||||
pub use mcp_connection_manager::SandboxState;
|
||||
|
|
|
|||
214
codex-rs/core/src/network_proxy_loader.rs
Normal file
214
codex-rs/core/src/network_proxy_loader.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::find_codex_home;
|
||||
use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::config_loader::LoaderOverrides;
|
||||
use crate::config_loader::load_config_layers_state;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_network_proxy::ConfigReloader;
|
||||
use codex_network_proxy::ConfigState;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkProxyConstraintError;
|
||||
use codex_network_proxy::NetworkProxyConstraints;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use codex_network_proxy::PartialNetworkProxyConfig;
|
||||
use codex_network_proxy::build_config_state;
|
||||
use codex_network_proxy::validate_policy_against_constraints;
|
||||
use std::path::PathBuf;
|
||||
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(
|
||||
&codex_home,
|
||||
None,
|
||||
&cli_overrides,
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
)
|
||||
.await
|
||||
.context("failed to load Codex config")?;
|
||||
|
||||
let cfg_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let merged_toml = config_layer_stack.effective_config();
|
||||
let config: NetworkProxyConfig = merged_toml
|
||||
.try_into()
|
||||
.context("failed to deserialize network proxy config")?;
|
||||
|
||||
let constraints = enforce_trusted_constraints(&config_layer_stack, &config)?;
|
||||
let layer_mtimes = collect_layer_mtimes(&config_layer_stack);
|
||||
let state = build_config_state(config, constraints, cfg_path)?;
|
||||
Ok((state, layer_mtimes))
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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();
|
||||
for layer in layers.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, false) {
|
||||
if is_user_controlled_layer(&layer.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let partial: PartialNetworkProxyConfig = 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
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LayerMtime {
|
||||
path: PathBuf,
|
||||
mtime: Option<std::time::SystemTime>,
|
||||
}
|
||||
|
||||
impl LayerMtime {
|
||||
fn new(path: PathBuf) -> 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_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)
|
||||
}
|
||||
}
|
||||
17
codex-rs/network-proxy-cli/Cargo.toml
Normal file
17
codex-rs/network-proxy-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "codex-network-proxy-cli"
|
||||
edition = "2024"
|
||||
version = { workspace = true }
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "codex-network-proxy"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-core = { workspace = true }
|
||||
codex-network-proxy = { path = "../network-proxy" }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing-subscriber = { workspace = true, features = ["fmt"] }
|
||||
19
codex-rs/network-proxy-cli/src/main.rs
Normal file
19
codex-rs/network-proxy-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use codex_core::network_proxy_loader;
|
||||
use codex_network_proxy::Args;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let args = Args::parse();
|
||||
let (state, reloader) = network_proxy_loader::build_network_proxy_state_and_reloader().await?;
|
||||
let state = Arc::new(NetworkProxyState::with_reloader(state, Arc::new(reloader)));
|
||||
let _ = args;
|
||||
let proxy = NetworkProxy::builder().state(state).build().await?;
|
||||
proxy.run().await?.wait().await
|
||||
}
|
||||
6
codex-rs/network-proxy/BUILD.bazel
Normal file
6
codex-rs/network-proxy/BUILD.bazel
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "network-proxy",
|
||||
crate_name = "codex_network_proxy",
|
||||
)
|
||||
|
|
@ -4,10 +4,6 @@ edition = "2024"
|
|||
version = { workspace = true }
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "codex-network-proxy"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "codex_network_proxy"
|
||||
path = "src/lib.rs"
|
||||
|
|
@ -19,16 +15,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 }
|
||||
globset = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["fmt"] }
|
||||
url = { workspace = true }
|
||||
rama-core = { version = "=0.3.0-alpha.4" }
|
||||
rama-http = { version = "=0.3.0-alpha.4" }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(¤t_cfg)?;
|
||||
// Reapply bind clamping for caller overrides so unix-socket proxying stays loopback-only.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue