mirror of
https://github.com/openai/codex.git
synced 2026-04-30 01:16:54 +00:00
add comments
This commit is contained in:
@@ -23,6 +23,8 @@ type ContextState = Arc<AppState>;
|
||||
type AdminContext = RamaContext<ContextState>;
|
||||
|
||||
pub async fn run_admin_api(state: Arc<AppState>, addr: SocketAddr) -> Result<()> {
|
||||
// Debug-only admin API (health/config/patterns/blocked + mode/reload). Policy is config-driven
|
||||
// and constraint-enforced; this endpoint should not become a second policy/approval plane.
|
||||
let listener = TcpListener::build_with_state(state)
|
||||
.bind(addr)
|
||||
.await
|
||||
|
||||
@@ -129,6 +129,8 @@ async fn http_connect_accept(
|
||||
};
|
||||
|
||||
if mode == NetworkMode::Limited && mitm_state.is_none() {
|
||||
// Limited mode is designed to be read-only. Without MITM, a CONNECT tunnel would hide the
|
||||
// inner HTTP method/headers from the proxy, effectively bypassing method policy.
|
||||
let _ = app_state
|
||||
.record_blocked(BlockedRequest::new(
|
||||
host.clone(),
|
||||
@@ -214,6 +216,9 @@ async fn http_plain_proxy(mut ctx: ProxyContext, req: Request) -> Result<Respons
|
||||
};
|
||||
|
||||
if let Some(socket_path) = req
|
||||
// `x-unix-socket` is an escape hatch for talking to local daemons. We keep it tightly
|
||||
// scoped: macOS-only + explicit allowlist, to avoid turning the proxy into a general local
|
||||
// capability-escalation mechanism.
|
||||
.headers()
|
||||
.get("x-unix-socket")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
|
||||
@@ -63,6 +63,9 @@ pub struct MitmState {
|
||||
|
||||
impl MitmState {
|
||||
pub fn new(cfg: &MitmConfig) -> Result<Self> {
|
||||
// MITM exists to make limited-mode HTTPS enforceable: once CONNECT is established, plain
|
||||
// proxying would lose visibility into the inner HTTP request. We generate/load a local CA
|
||||
// and issue per-host leaf certs so we can terminate TLS and apply policy.
|
||||
let (ca_cert_pem, ca_key_pem) = load_or_create_ca(cfg)?;
|
||||
let ca_key = KeyPair::from_pem(&ca_key_pem).context("failed to parse CA key")?;
|
||||
let issuer: Issuer<'static, KeyPair> =
|
||||
@@ -71,6 +74,7 @@ impl MitmState {
|
||||
let tls_config = rama::tls::rustls::client::TlsConnectorData::new_http_auto()
|
||||
.context("create upstream TLS config")?;
|
||||
let upstream = rama::http::client::EasyHttpWebClient::builder()
|
||||
// Use a direct transport connector (no upstream proxy) to avoid proxy loops.
|
||||
.with_default_transport_connector()
|
||||
.without_tls_proxy_support()
|
||||
.without_proxy_support()
|
||||
@@ -449,6 +453,7 @@ fn load_or_create_ca(cfg: &MitmConfig) -> Result<(String, String)> {
|
||||
}
|
||||
|
||||
let (cert_pem, key_pem) = generate_ca()?;
|
||||
// The CA key is a high-value secret. Ensure it is not world-readable. The cert can be.
|
||||
write_private_file(cert_path, cert_pem.as_bytes(), 0o644)?;
|
||||
write_private_file(key_path, key_pem.as_bytes(), 0o600)?;
|
||||
let cert_path = cert_path.display();
|
||||
|
||||
@@ -86,6 +86,8 @@ impl AppState {
|
||||
}
|
||||
|
||||
pub async fn current_cfg(&self) -> Result<Config> {
|
||||
// Callers treat `AppState` as a live view of policy. We reload-on-demand so edits to
|
||||
// `config.toml` (including Codex-managed writes) take effect without a restart.
|
||||
self.reload_if_needed().await?;
|
||||
let guard = self.state.read().await;
|
||||
Ok(guard.config.clone())
|
||||
@@ -106,6 +108,8 @@ impl AppState {
|
||||
let blocked = guard.blocked.clone();
|
||||
match build_config_state().await {
|
||||
Ok(mut new_state) => {
|
||||
// Policy changes are operationally sensitive; logging diffs makes changes traceable
|
||||
// without needing to dump full config blobs (which can include unrelated settings).
|
||||
log_policy_changes(&previous_cfg, &new_state.config);
|
||||
new_state.blocked = blocked;
|
||||
*guard = new_state;
|
||||
@@ -124,6 +128,10 @@ impl AppState {
|
||||
pub async fn host_blocked(&self, host: &str) -> Result<(bool, String)> {
|
||||
self.reload_if_needed().await?;
|
||||
let guard = self.state.read().await;
|
||||
// Decision order matters:
|
||||
// 1) explicit deny always wins
|
||||
// 2) local/loopback is opt-in (defense-in-depth)
|
||||
// 3) allowlist is enforced when configured
|
||||
if guard.deny_set.is_match(host) {
|
||||
return Ok((true, "denied".to_string()));
|
||||
}
|
||||
@@ -223,6 +231,8 @@ impl AppState {
|
||||
}
|
||||
|
||||
async fn build_config_state() -> Result<ConfigState> {
|
||||
// Load config through `codex-core` so we inherit the same layer ordering and semantics as the
|
||||
// rest of Codex (system/managed layers, user layers, session flags, etc.).
|
||||
let codex_cfg = ConfigBuilder::default()
|
||||
.build()
|
||||
.await
|
||||
@@ -230,13 +240,19 @@ async fn build_config_state() -> Result<ConfigState> {
|
||||
|
||||
let cfg_path = codex_cfg.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 = codex_cfg.config_layer_stack.effective_config();
|
||||
let mut config: Config = 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.
|
||||
enforce_trusted_constraints(&codex_cfg.config_layer_stack, &config)?;
|
||||
|
||||
// Permit relative MITM paths for ergonomics; resolve them relative to the directory containing
|
||||
// `config.toml` so the config is relocatable.
|
||||
resolve_mitm_paths(&mut config, &cfg_path);
|
||||
let mtime = cfg_path.metadata().and_then(|m| m.modified()).ok();
|
||||
let deny_set = compile_globset(&config.network_proxy.policy.denied_domains)?;
|
||||
@@ -324,6 +340,8 @@ fn network_proxy_constraints_from_trusted_layers(
|
||||
for layer in layers
|
||||
.get_layers(codex_core::config_loader::ConfigLayerStackOrdering::LowestPrecedenceFirst)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
@@ -506,6 +524,8 @@ fn compile_globset(patterns: &[String]) -> Result<GlobSet> {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
let mut seen = HashSet::new();
|
||||
for pattern in patterns {
|
||||
// Operator ergonomics: `*.example.com` usually intends to include both `a.example.com` and
|
||||
// the apex `example.com`. We expand that here so policy matches expectation.
|
||||
let mut expanded = Vec::with_capacity(2);
|
||||
expanded.push(pattern.as_str());
|
||||
if let Some(apex) = pattern.strip_prefix("*.") {
|
||||
|
||||
Reference in New Issue
Block a user