mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
488 lines
17 KiB
Rust
488 lines
17 KiB
Rust
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;
|
|
|
|
pub use crate::runtime::BlockedRequest;
|
|
pub use crate::runtime::BlockedRequestArgs;
|
|
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)))
|
|
}
|
|
|
|
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")?;
|
|
|
|
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 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);
|
|
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,
|
|
))
|
|
}
|
|
|
|
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(
|
|
config: &NetworkProxyConfig,
|
|
constraints: &NetworkProxyConstraints,
|
|
) -> std::result::Result<(), ConstraintError> {
|
|
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,
|
|
}
|
|
}
|
|
|
|
fn validate<T>(
|
|
candidate: T,
|
|
validator: impl FnOnce(&T) -> std::result::Result<(), ConstraintError>,
|
|
) -> std::result::Result<(), ConstraintError> {
|
|
validator(&candidate)
|
|
}
|
|
|
|
let enabled = config.network.enabled;
|
|
if let Some(max_enabled) = constraints.enabled {
|
|
validate(enabled, move |candidate| {
|
|
if *candidate && !max_enabled {
|
|
Err(invalid_value(
|
|
"network.enabled",
|
|
"true",
|
|
"false (disabled by managed config)",
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})?;
|
|
}
|
|
|
|
if let Some(max_mode) = constraints.mode {
|
|
validate(config.network.mode, move |candidate| {
|
|
if network_mode_rank(*candidate) > network_mode_rank(max_mode) {
|
|
Err(invalid_value(
|
|
"network.mode",
|
|
format!("{candidate:?}"),
|
|
format!("{max_mode:?} or more restrictive"),
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})?;
|
|
}
|
|
|
|
let allow_upstream_proxy = constraints.allow_upstream_proxy;
|
|
validate(
|
|
config.network.allow_upstream_proxy,
|
|
move |candidate| match allow_upstream_proxy {
|
|
Some(true) | None => Ok(()),
|
|
Some(false) => {
|
|
if *candidate {
|
|
Err(invalid_value(
|
|
"network.allow_upstream_proxy",
|
|
"true",
|
|
"false (disabled by managed config)",
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)?;
|
|
|
|
let allow_non_loopback_admin = constraints.dangerously_allow_non_loopback_admin;
|
|
validate(
|
|
config.network.dangerously_allow_non_loopback_admin,
|
|
move |candidate| match allow_non_loopback_admin {
|
|
Some(true) | None => Ok(()),
|
|
Some(false) => {
|
|
if *candidate {
|
|
Err(invalid_value(
|
|
"network.dangerously_allow_non_loopback_admin",
|
|
"true",
|
|
"false (disabled by managed config)",
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)?;
|
|
|
|
let allow_non_loopback_proxy = constraints.dangerously_allow_non_loopback_proxy;
|
|
validate(
|
|
config.network.dangerously_allow_non_loopback_proxy,
|
|
move |candidate| match allow_non_loopback_proxy {
|
|
Some(true) | None => Ok(()),
|
|
Some(false) => {
|
|
if *candidate {
|
|
Err(invalid_value(
|
|
"network.dangerously_allow_non_loopback_proxy",
|
|
"true",
|
|
"false (disabled by managed config)",
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
},
|
|
)?;
|
|
|
|
if let Some(allow_local_binding) = constraints.allow_local_binding {
|
|
validate(config.network.allow_local_binding, move |candidate| {
|
|
if *candidate && !allow_local_binding {
|
|
Err(invalid_value(
|
|
"network.allow_local_binding",
|
|
"true",
|
|
"false (disabled by managed config)",
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})?;
|
|
}
|
|
|
|
if let Some(allowed_domains) = &constraints.allowed_domains {
|
|
let managed_patterns: Vec<DomainPattern> = allowed_domains
|
|
.iter()
|
|
.map(|entry| DomainPattern::parse_for_constraints(entry))
|
|
.collect();
|
|
validate(config.network.allowed_domains.clone(), move |candidate| {
|
|
let mut invalid = Vec::new();
|
|
for entry in candidate {
|
|
let candidate_pattern = DomainPattern::parse_for_constraints(entry);
|
|
if !managed_patterns
|
|
.iter()
|
|
.any(|managed| managed.allows(&candidate_pattern))
|
|
{
|
|
invalid.push(entry.clone());
|
|
}
|
|
}
|
|
if invalid.is_empty() {
|
|
Ok(())
|
|
} else {
|
|
Err(invalid_value(
|
|
"network.allowed_domains",
|
|
format!("{invalid:?}"),
|
|
"subset of managed allowed_domains",
|
|
))
|
|
}
|
|
})?;
|
|
}
|
|
|
|
if let Some(denied_domains) = &constraints.denied_domains {
|
|
let required_set: HashSet<String> = denied_domains
|
|
.iter()
|
|
.map(|s| s.to_ascii_lowercase())
|
|
.collect();
|
|
validate(config.network.denied_domains.clone(), move |candidate| {
|
|
let candidate_set: HashSet<String> =
|
|
candidate.iter().map(|s| s.to_ascii_lowercase()).collect();
|
|
let missing: Vec<String> = required_set
|
|
.iter()
|
|
.filter(|entry| !candidate_set.contains(*entry))
|
|
.cloned()
|
|
.collect();
|
|
if missing.is_empty() {
|
|
Ok(())
|
|
} else {
|
|
Err(invalid_value(
|
|
"network.denied_domains",
|
|
"missing managed denied_domains entries",
|
|
format!("{missing:?}"),
|
|
))
|
|
}
|
|
})?;
|
|
}
|
|
|
|
if let Some(allow_unix_sockets) = &constraints.allow_unix_sockets {
|
|
let allowed_set: HashSet<String> = allow_unix_sockets
|
|
.iter()
|
|
.map(|s| s.to_ascii_lowercase())
|
|
.collect();
|
|
validate(
|
|
config.network.allow_unix_sockets.clone(),
|
|
move |candidate| {
|
|
let mut invalid = Vec::new();
|
|
for entry in candidate {
|
|
if !allowed_set.contains(&entry.to_ascii_lowercase()) {
|
|
invalid.push(entry.clone());
|
|
}
|
|
}
|
|
if invalid.is_empty() {
|
|
Ok(())
|
|
} else {
|
|
Err(invalid_value(
|
|
"network.allow_unix_sockets",
|
|
format!("{invalid:?}"),
|
|
"subset of managed allow_unix_sockets",
|
|
))
|
|
}
|
|
},
|
|
)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn network_mode_rank(mode: NetworkMode) -> u8 {
|
|
match mode {
|
|
NetworkMode::Limited => 0,
|
|
NetworkMode::Full => 1,
|
|
}
|
|
}
|