mirror of
https://github.com/openai/codex.git
synced 2026-05-01 03:42:05 +03:00
feat: introducing a network sandbox proxy (#8442)
This add a new crate, `codex-network-proxy`, a local network proxy service used by Codex to enforce fine-grained network policy (domain allow/deny) and to surface blocked network events for interactive approvals. - New crate: `codex-rs/network-proxy/` (`codex-network-proxy` binary + library) - Core capabilities: - HTTP proxy support (including CONNECT tunneling) - SOCKS5 proxy support (in the later PR) - policy evaluation (allowed/denied domain lists; denylist wins; wildcard support) - small admin API for polling/reload/mode changes - optional MITM support for HTTPS CONNECT to enforce “limited mode” method restrictions (later PR) Will follow up integration with codex in subsequent PRs. ## Testing - `cd codex-rs && cargo build -p codex-network-proxy` - `cd codex-rs && cargo run -p codex-network-proxy -- proxy`
This commit is contained in:
160
codex-rs/network-proxy/src/admin.rs
Normal file
160
codex-rs/network-proxy/src/admin.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use crate::config::NetworkMode;
|
||||
use crate::responses::json_response;
|
||||
use crate::responses::text_response;
|
||||
use crate::state::NetworkProxyState;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use rama_core::rt::Executor;
|
||||
use rama_core::service::service_fn;
|
||||
use rama_http::Body;
|
||||
use rama_http::Request;
|
||||
use rama_http::Response;
|
||||
use rama_http::StatusCode;
|
||||
use rama_http_backend::server::HttpServer;
|
||||
use rama_tcp::server::TcpListener;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
|
||||
pub async fn run_admin_api(state: Arc<NetworkProxyState>, 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()
|
||||
.bind(addr)
|
||||
.await
|
||||
// See `http_proxy.rs` for details on why we wrap `BoxError` before converting to anyhow.
|
||||
.map_err(rama_core::error::OpaqueError::from)
|
||||
.map_err(anyhow::Error::from)
|
||||
.with_context(|| format!("bind admin API: {addr}"))?;
|
||||
|
||||
let server_state = state.clone();
|
||||
let server = HttpServer::auto(Executor::new()).service(service_fn(move |req| {
|
||||
let state = server_state.clone();
|
||||
async move { handle_admin_request(state, req).await }
|
||||
}));
|
||||
info!("admin API listening on {addr}");
|
||||
listener.serve(server).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_admin_request(
|
||||
state: Arc<NetworkProxyState>,
|
||||
req: Request,
|
||||
) -> Result<Response, Infallible> {
|
||||
const MODE_BODY_LIMIT: usize = 8 * 1024;
|
||||
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
let response = match (method.as_str(), path.as_str()) {
|
||||
("GET", "/health") => Response::new(Body::from("ok")),
|
||||
("GET", "/config") => match state.current_cfg().await {
|
||||
Ok(cfg) => json_response(&cfg),
|
||||
Err(err) => {
|
||||
error!("failed to load config: {err}");
|
||||
text_response(StatusCode::INTERNAL_SERVER_ERROR, "error")
|
||||
}
|
||||
},
|
||||
("GET", "/patterns") => match state.current_patterns().await {
|
||||
Ok((allow, deny)) => json_response(&PatternsResponse {
|
||||
allowed: allow,
|
||||
denied: deny,
|
||||
}),
|
||||
Err(err) => {
|
||||
error!("failed to load patterns: {err}");
|
||||
text_response(StatusCode::INTERNAL_SERVER_ERROR, "error")
|
||||
}
|
||||
},
|
||||
("GET", "/blocked") => match state.drain_blocked().await {
|
||||
Ok(blocked) => json_response(&BlockedResponse { blocked }),
|
||||
Err(err) => {
|
||||
error!("failed to read blocked queue: {err}");
|
||||
text_response(StatusCode::INTERNAL_SERVER_ERROR, "error")
|
||||
}
|
||||
},
|
||||
("POST", "/mode") => {
|
||||
let mut body = req.into_body();
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
loop {
|
||||
let chunk = match body.chunk().await {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => {
|
||||
error!("failed to read mode body: {err}");
|
||||
return Ok(text_response(StatusCode::BAD_REQUEST, "invalid body"));
|
||||
}
|
||||
};
|
||||
let Some(chunk) = chunk else {
|
||||
break;
|
||||
};
|
||||
|
||||
if buf.len().saturating_add(chunk.len()) > MODE_BODY_LIMIT {
|
||||
return Ok(text_response(
|
||||
StatusCode::PAYLOAD_TOO_LARGE,
|
||||
"body too large",
|
||||
));
|
||||
}
|
||||
buf.extend_from_slice(&chunk);
|
||||
}
|
||||
|
||||
if buf.is_empty() {
|
||||
return Ok(text_response(StatusCode::BAD_REQUEST, "missing body"));
|
||||
}
|
||||
let update: ModeUpdate = match serde_json::from_slice(&buf) {
|
||||
Ok(update) => update,
|
||||
Err(err) => {
|
||||
error!("failed to parse mode update: {err}");
|
||||
return Ok(text_response(StatusCode::BAD_REQUEST, "invalid json"));
|
||||
}
|
||||
};
|
||||
match state.set_network_mode(update.mode).await {
|
||||
Ok(()) => json_response(&ModeUpdateResponse {
|
||||
status: "ok",
|
||||
mode: update.mode,
|
||||
}),
|
||||
Err(err) => {
|
||||
error!("mode update failed: {err}");
|
||||
text_response(StatusCode::INTERNAL_SERVER_ERROR, "mode update failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
("POST", "/reload") => match state.force_reload().await {
|
||||
Ok(()) => json_response(&ReloadResponse { status: "reloaded" }),
|
||||
Err(err) => {
|
||||
error!("reload failed: {err}");
|
||||
text_response(StatusCode::INTERNAL_SERVER_ERROR, "reload failed")
|
||||
}
|
||||
},
|
||||
_ => text_response(StatusCode::NOT_FOUND, "not found"),
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModeUpdate {
|
||||
mode: NetworkMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct PatternsResponse {
|
||||
allowed: Vec<String>,
|
||||
denied: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BlockedResponse<T> {
|
||||
blocked: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ModeUpdateResponse {
|
||||
status: &'static str,
|
||||
mode: NetworkMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ReloadResponse {
|
||||
status: &'static str,
|
||||
}
|
||||
Reference in New Issue
Block a user