add comments

This commit is contained in:
viyatb-oai
2025-12-23 18:40:20 -08:00
parent 6f4edec9f1
commit 2d7980340d
4 changed files with 32 additions and 0 deletions

View File

@@ -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

View File

@@ -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())

View File

@@ -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();

View File

@@ -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("*.") {