remove major func signature changes

This commit is contained in:
viyatb-oai
2025-12-19 08:56:32 -08:00
parent 78a5014430
commit f7648cf2b5
23 changed files with 677 additions and 149 deletions
@@ -659,6 +659,7 @@ mod tests {
parsed_cmd: vec![ParsedCommand::Unknown {
cmd: "echo hello".to_string(),
}],
network_preflight_only: false,
};
let request = ServerRequest::ExecCommandApproval {
request_id: RequestId::Integer(7),
@@ -680,7 +681,8 @@ mod tests {
"type": "unknown",
"cmd": "echo hello"
}
]
],
"networkPreflightOnly": false
}
}),
serde_json::to_value(&request)?,
@@ -227,6 +227,8 @@ pub struct ExecCommandApprovalParams {
pub cwd: PathBuf,
pub reason: Option<String>,
pub parsed_cmd: Vec<ParsedCommand>,
#[serde(default)]
pub network_preflight_only: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -182,6 +182,7 @@ pub(crate) async fn apply_bespoke_event_handling(
reason,
proposed_execpolicy_amendment,
parsed_cmd,
network_preflight_only,
..
}) => match api_version {
ApiVersion::V1 => {
@@ -192,6 +193,7 @@ pub(crate) async fn apply_bespoke_event_handling(
cwd,
reason,
parsed_cmd,
network_preflight_only,
};
let rx = outgoing
.send_request(ServerRequestPayload::ExecCommandApproval(params))
@@ -1172,7 +1172,15 @@ impl CodexMessageProcessor {
}
let cwd = params.cwd.unwrap_or_else(|| self.config.cwd.clone());
let env = create_env(&self.config.shell_environment_policy);
let effective_policy = params
.sandbox_policy
.map(|policy| policy.to_core())
.unwrap_or_else(|| self.config.sandbox_policy.clone());
let env = create_env(
&self.config.shell_environment_policy,
&effective_policy,
&self.config.network_proxy,
);
let timeout_ms = params
.timeout_ms
.and_then(|timeout_ms| u64::try_from(timeout_ms).ok());
@@ -1186,11 +1194,6 @@ impl CodexMessageProcessor {
arg0: None,
};
let effective_policy = params
.sandbox_policy
.map(|policy| policy.to_core())
.unwrap_or_else(|| self.config.sandbox_policy.clone());
let codex_linux_sandbox_exe = self.config.codex_linux_sandbox_exe.clone();
let outgoing = self.outgoing.clone();
let req_id = request_id;
+5 -1
View File
@@ -130,7 +130,11 @@ async fn run_command_under_sandbox(
let sandbox_policy_cwd = cwd.clone();
let stdio_policy = StdioPolicy::Inherit;
let env = create_env(&config.shell_environment_policy);
let env = create_env(
&config.shell_environment_policy,
&config.sandbox_policy,
&config.network_proxy,
);
// Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
if let SandboxType::Windows = sandbox_type {
+13 -17
View File
@@ -393,15 +393,6 @@ impl TurnContext {
}
}
pub(crate) struct CommandApprovalRequest {
pub(crate) call_id: String,
pub(crate) command: Vec<String>,
pub(crate) cwd: PathBuf,
pub(crate) reason: Option<String>,
pub(crate) network_preflight_only: bool,
pub(crate) proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
}
#[allow(dead_code)]
#[derive(Clone)]
pub(crate) struct SessionConfiguration {
@@ -1106,7 +1097,12 @@ impl Session {
pub async fn request_command_approval(
&self,
turn_context: &TurnContext,
request: CommandApprovalRequest,
call_id: String,
command: Vec<String>,
cwd: PathBuf,
reason: Option<String>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
network_preflight_only: bool,
) -> ReviewDecision {
let sub_id = turn_context.sub_id.clone();
// Add the tx_approve callback to the map before sending the request.
@@ -1126,16 +1122,16 @@ impl Session {
warn!("Overwriting existing pending approval for sub_id: {event_id}");
}
let parsed_cmd = parse_command(&request.command);
let parsed_cmd = parse_command(&command);
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
call_id: request.call_id,
call_id,
turn_id: turn_context.sub_id.clone(),
command: request.command,
cwd: request.cwd,
reason: request.reason,
proposed_execpolicy_amendment: request.proposed_execpolicy_amendment,
command,
cwd,
reason,
proposed_execpolicy_amendment,
parsed_cmd,
network_preflight_only: request.network_preflight_only,
network_preflight_only,
});
self.send_event(turn_context, event).await;
rx_approve.await.unwrap_or_default()
+6 -8
View File
@@ -277,14 +277,12 @@ async fn handle_exec_approval(
// Race approval with cancellation and timeout to avoid hangs.
let approval_fut = parent_session.request_command_approval(
parent_ctx,
crate::codex::CommandApprovalRequest {
call_id: parent_ctx.sub_id.clone(),
command: event.command,
cwd: event.cwd,
reason: event.reason,
network_preflight_only: event.network_preflight_only,
proposed_execpolicy_amendment: event.proposed_execpolicy_amendment,
},
parent_ctx.sub_id.clone(),
event.command,
event.cwd,
event.reason,
event.proposed_execpolicy_amendment,
event.network_preflight_only,
);
let decision = await_approval_with_cancel(
approval_fut,
+12 -2
View File
@@ -865,8 +865,18 @@ fn ensure_default_no_proxy_entries(entries: &mut Vec<String>) {
}
}
fn default_no_proxy_entries() -> [&'static str; 3] {
["localhost", "127.0.0.1", "::1"]
fn default_no_proxy_entries() -> [&'static str; 9] {
[
"localhost",
"127.0.0.1",
"::1",
"*.local",
".local",
"169.254.0.0/16",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
]
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
+248 -41
View File
@@ -7,6 +7,8 @@ use crate::protocol::SandboxPolicy;
use std::collections::HashMap;
use std::collections::HashSet;
const DEFAULT_SOCKS_PROXY_PORT: u16 = 8081;
/// Construct an environment map based on the rules in the specified policy. The
/// resulting map can be passed directly to `Command::envs()` after calling
/// `env_clear()` to ensure no unintended variables are leaked to the spawned
@@ -14,16 +16,12 @@ use std::collections::HashSet;
///
/// The derivation follows the algorithm documented in the struct-level comment
/// for [`ShellEnvironmentPolicy`].
pub fn create_env(policy: &ShellEnvironmentPolicy) -> HashMap<String, String> {
populate_env(std::env::vars(), policy)
}
pub fn create_env_with_network_proxy(
pub fn create_env(
policy: &ShellEnvironmentPolicy,
sandbox_policy: &SandboxPolicy,
network_proxy: &NetworkProxyConfig,
) -> HashMap<String, String> {
let mut env_map = create_env(policy);
let mut env_map = populate_env(std::env::vars(), policy);
if should_apply_network_proxy(network_proxy, sandbox_policy) {
apply_network_proxy_env(&mut env_map, network_proxy);
}
@@ -97,54 +95,210 @@ fn should_apply_network_proxy(
}
}
fn apply_network_proxy_env(
env_map: &mut HashMap<String, String>,
#[derive(Clone, Debug)]
struct ProxyEndpoint {
host: String,
port: u16,
}
#[derive(Default)]
struct ProxyEndpoints {
http: Option<ProxyEndpoint>,
socks: Option<ProxyEndpoint>,
}
fn proxy_env_entries(
network_proxy: &NetworkProxyConfig,
) {
let proxy_url = network_proxy.proxy_url.trim();
if !proxy_url.is_empty() {
endpoints: &ProxyEndpoints,
) -> Vec<String> {
let mut entries = Vec::new();
let no_proxy = normalize_no_proxy_value(&network_proxy.no_proxy);
if !no_proxy.is_empty() {
entries.push(format!("NO_PROXY={no_proxy}"));
entries.push(format!("no_proxy={no_proxy}"));
}
let http_proxy_url = endpoints
.http
.as_ref()
.map(|endpoint| format_proxy_url("http", endpoint));
let socks_proxy_url = endpoints
.socks
.as_ref()
.map(|endpoint| format_proxy_url("socks5h", endpoint));
let socks_host_port = endpoints
.socks
.as_ref()
.map(|endpoint| format_host_port(&endpoint.host, endpoint.port));
if let Some(http_proxy_url) = http_proxy_url.as_ref() {
for key in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"] {
entries.push(format!("{key}={http_proxy_url}"));
}
for key in [
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
"YARN_HTTP_PROXY",
"YARN_HTTPS_PROXY",
"npm_config_http_proxy",
"npm_config_https_proxy",
"npm_config_proxy",
] {
env_map.insert(key.to_string(), proxy_url.to_string());
entries.push(format!("{key}={http_proxy_url}"));
}
env_map.insert("ELECTRON_GET_USE_PROXY".to_string(), "true".to_string());
entries.push("ELECTRON_GET_USE_PROXY=true".to_string());
}
if let Some((host, port)) = network_proxy::proxy_host_port(proxy_url) {
let gradle_opts = format!(
"-Dhttp.proxyHost={host} -Dhttp.proxyPort={port} -Dhttps.proxyHost={host} -Dhttps.proxyPort={port}"
);
match env_map.get_mut("GRADLE_OPTS") {
Some(existing) => {
if !existing.contains("http.proxyHost") && !existing.contains("https.proxyHost")
{
if !existing.ends_with(' ') {
existing.push(' ');
}
existing.push_str(&gradle_opts);
}
}
None => {
env_map.insert("GRADLE_OPTS".to_string(), gradle_opts);
}
}
if let Some(socks_proxy_url) = socks_proxy_url.as_ref() {
entries.push(format!("ALL_PROXY={socks_proxy_url}"));
entries.push(format!("all_proxy={socks_proxy_url}"));
}
if let Some(socks_host_port) = socks_host_port.as_ref() {
#[cfg(target_os = "macos")]
entries.push(format!(
"GIT_SSH_COMMAND=ssh -o ProxyCommand='nc -X 5 -x {socks_host_port} %h %p'"
));
if let Some(socks_proxy_url) = socks_proxy_url.as_ref() {
entries.push(format!("FTP_PROXY={socks_proxy_url}"));
entries.push(format!("ftp_proxy={socks_proxy_url}"));
}
entries.push(format!("RSYNC_PROXY={socks_host_port}"));
}
let docker_proxy = endpoints.http.as_ref().or(endpoints.socks.as_ref());
if let Some(endpoint) = docker_proxy {
let docker_proxy_url = format_proxy_url("http", endpoint);
entries.push(format!("DOCKER_HTTP_PROXY={docker_proxy_url}"));
entries.push(format!("DOCKER_HTTPS_PROXY={docker_proxy_url}"));
}
if let Some(endpoint) = endpoints.http.as_ref() {
entries.push("CLOUDSDK_PROXY_TYPE=https".to_string());
entries.push("CLOUDSDK_PROXY_ADDRESS=localhost".to_string());
let port = endpoint.port;
entries.push(format!("CLOUDSDK_PROXY_PORT={port}"));
}
if let Some(socks_proxy_url) = socks_proxy_url.as_ref() {
entries.push(format!("GRPC_PROXY={socks_proxy_url}"));
entries.push(format!("grpc_proxy={socks_proxy_url}"));
}
entries
}
fn resolve_proxy_endpoints(network_proxy: &NetworkProxyConfig) -> ProxyEndpoints {
let proxy_url = network_proxy.proxy_url.trim();
if proxy_url.is_empty() {
return ProxyEndpoints::default();
}
let Some((host, port)) = network_proxy::proxy_host_port(proxy_url) else {
return ProxyEndpoints::default();
};
let Some(port) = normalize_proxy_port(port) else {
return ProxyEndpoints::default();
};
let (host, is_loopback) = normalize_proxy_host(&host);
let is_socks = proxy_url_scheme(proxy_url)
.map(|scheme| scheme.to_ascii_lowercase().starts_with("socks"))
.unwrap_or(false);
let http = if is_socks {
None
} else {
Some(ProxyEndpoint {
host: host.clone(),
port,
})
};
let mut socks = if is_socks {
Some(ProxyEndpoint {
host: host.clone(),
port,
})
} else {
None
};
if socks.is_none() && is_loopback {
socks = Some(ProxyEndpoint {
host: "localhost".to_string(),
port: DEFAULT_SOCKS_PROXY_PORT,
});
}
ProxyEndpoints { http, socks }
}
fn proxy_url_scheme(proxy_url: &str) -> Option<&str> {
proxy_url.split_once("://").map(|(scheme, _)| scheme)
}
fn normalize_proxy_host(host: &str) -> (String, bool) {
let is_loopback =
host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1" || host == "::1";
if is_loopback {
("localhost".to_string(), true)
} else {
(host.to_string(), false)
}
}
fn normalize_proxy_port(port: i64) -> Option<u16> {
if (1..=u16::MAX as i64).contains(&port) {
Some(port as u16)
} else {
None
}
}
fn format_proxy_url(scheme: &str, endpoint: &ProxyEndpoint) -> String {
let host = &endpoint.host;
let port = endpoint.port;
if endpoint.host.contains(':') {
format!("{scheme}://[{host}]:{port}")
} else {
format!("{scheme}://{host}:{port}")
}
}
fn format_host_port(host: &str, port: u16) -> String {
if host.contains(':') {
format!("[{host}]:{port}")
} else {
format!("{host}:{port}")
}
}
fn apply_network_proxy_env(
env_map: &mut HashMap<String, String>,
network_proxy: &NetworkProxyConfig,
) {
let endpoints = resolve_proxy_endpoints(network_proxy);
for entry in proxy_env_entries(network_proxy, &endpoints) {
if let Some((key, value)) = entry.split_once('=') {
env_map.insert(key.to_string(), value.to_string());
}
}
let no_proxy = normalize_no_proxy_value(&network_proxy.no_proxy);
if !no_proxy.is_empty() {
env_map.insert("NO_PROXY".to_string(), no_proxy.clone());
env_map.insert("no_proxy".to_string(), no_proxy);
if let Some(endpoint) = endpoints.http {
let host = &endpoint.host;
let port = endpoint.port;
let gradle_opts = format!(
"-Dhttp.proxyHost={host} -Dhttp.proxyPort={port} -Dhttps.proxyHost={host} -Dhttps.proxyPort={port}"
);
match env_map.get_mut("GRADLE_OPTS") {
Some(existing) => {
if !existing.contains("http.proxyHost") && !existing.contains("https.proxyHost") {
if !existing.ends_with(' ') {
existing.push(' ');
}
existing.push_str(&gradle_opts);
}
}
None => {
env_map.insert("GRADLE_OPTS".to_string(), gradle_opts);
}
}
}
network_proxy::apply_mitm_ca_env_if_enabled(env_map, network_proxy);
@@ -165,8 +319,10 @@ fn normalize_no_proxy_value(entries: &[String]) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::types::NetworkProxyMode;
use crate::config::types::ShellEnvironmentPolicyInherit;
use maplit::hashmap;
use pretty_assertions::assert_eq;
fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
@@ -285,4 +441,55 @@ mod tests {
};
assert_eq!(result, expected);
}
#[test]
fn proxy_env_entries_are_deterministic() {
let network_proxy = NetworkProxyConfig {
enabled: true,
proxy_url: "http://localhost:3128".to_string(),
admin_url: "http://localhost:8080".to_string(),
mode: NetworkProxyMode::Full,
no_proxy: vec!["localhost".to_string(), "127.0.0.1".to_string()],
prompt_on_block: true,
poll_interval_ms: 1000,
mitm_ca_cert_path: None,
};
let endpoints = resolve_proxy_endpoints(&network_proxy);
let entries = proxy_env_entries(&network_proxy, &endpoints);
let mut expected = vec![
"NO_PROXY=localhost,127.0.0.1".to_string(),
"no_proxy=localhost,127.0.0.1".to_string(),
"HTTP_PROXY=http://localhost:3128".to_string(),
"HTTPS_PROXY=http://localhost:3128".to_string(),
"http_proxy=http://localhost:3128".to_string(),
"https_proxy=http://localhost:3128".to_string(),
"YARN_HTTP_PROXY=http://localhost:3128".to_string(),
"YARN_HTTPS_PROXY=http://localhost:3128".to_string(),
"npm_config_http_proxy=http://localhost:3128".to_string(),
"npm_config_https_proxy=http://localhost:3128".to_string(),
"npm_config_proxy=http://localhost:3128".to_string(),
"ELECTRON_GET_USE_PROXY=true".to_string(),
"ALL_PROXY=socks5h://localhost:8081".to_string(),
"all_proxy=socks5h://localhost:8081".to_string(),
];
#[cfg(target_os = "macos")]
expected.push(
"GIT_SSH_COMMAND=ssh -o ProxyCommand='nc -X 5 -x localhost:8081 %h %p'".to_string(),
);
expected.extend([
"FTP_PROXY=socks5h://localhost:8081".to_string(),
"ftp_proxy=socks5h://localhost:8081".to_string(),
"RSYNC_PROXY=localhost:8081".to_string(),
"DOCKER_HTTP_PROXY=http://localhost:3128".to_string(),
"DOCKER_HTTPS_PROXY=http://localhost:3128".to_string(),
"CLOUDSDK_PROXY_TYPE=https".to_string(),
"CLOUDSDK_PROXY_ADDRESS=localhost".to_string(),
"CLOUDSDK_PROXY_PORT=3128".to_string(),
"GRPC_PROXY=socks5h://localhost:8081".to_string(),
"grpc_proxy=socks5h://localhost:8081".to_string(),
]);
assert_eq!(entries, expected);
}
}
+6 -2
View File
@@ -420,14 +420,18 @@ struct NetworkProxySection {
}
#[derive(Default, Deserialize)]
struct NetworkPolicy {
pub(crate) struct NetworkPolicy {
#[serde(default, rename = "allowedDomains")]
allowed_domains: Vec<String>,
#[serde(default, rename = "deniedDomains")]
denied_domains: Vec<String>,
#[serde(default, rename = "allowUnixSockets")]
pub(crate) allow_unix_sockets: Vec<String>,
#[serde(default, rename = "allowLocalBinding")]
pub(crate) allow_local_binding: bool,
}
fn load_network_policy(config_path: &Path) -> Result<NetworkPolicy> {
pub(crate) fn load_network_policy(config_path: &Path) -> Result<NetworkPolicy> {
if !config_path.exists() {
return Ok(NetworkPolicy::default());
}
+1 -1
View File
@@ -118,7 +118,7 @@ impl SandboxManager {
let mut seatbelt_env = HashMap::new();
seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
let mut args =
create_seatbelt_command_args(command.clone(), policy, sandbox_policy_cwd);
create_seatbelt_command_args(command.clone(), policy, sandbox_policy_cwd, &env);
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
full_command.append(&mut args);
+278 -9
View File
@@ -1,18 +1,30 @@
#![cfg(target_os = "macos")]
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::ffi::CStr;
use std::path::Path;
use std::path::PathBuf;
use tokio::process::Child;
use crate::config;
use crate::network_proxy;
use crate::protocol::SandboxPolicy;
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");
const MACOS_SEATBELT_NETWORK_POLICY_BASE: &str = include_str!("seatbelt_network_policy.sbpl");
const PROXY_ENV_KEYS: &[&str] = &[
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
];
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
/// to defend against an attacker trying to inject a malicious version on the
@@ -28,7 +40,7 @@ pub async fn spawn_command_under_seatbelt(
stdio_policy: StdioPolicy,
mut env: HashMap<String, String>,
) -> std::io::Result<Child> {
let args = create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd);
let args = create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd, &env);
let arg0 = None;
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
spawn_child_async(
@@ -43,10 +55,192 @@ pub async fn spawn_command_under_seatbelt(
.await
}
fn proxy_allowlist_from_env(env: &HashMap<String, String>) -> Vec<String> {
let mut allowlist = Vec::new();
let mut seen = HashSet::new();
for key in PROXY_ENV_KEYS {
let Some(proxy_url) = env.get(*key) else {
continue;
};
let Some((host, port)) = network_proxy::proxy_host_port(proxy_url) else {
continue;
};
for entry in proxy_allowlist_entries(&host, port) {
if seen.insert(entry.clone()) {
allowlist.push(entry);
}
}
}
allowlist
}
fn proxy_allowlist_entries(host: &str, port: i64) -> Vec<String> {
let mut entries = Vec::new();
let is_loopback = is_loopback_host(host);
if is_loopback {
for candidate in ["localhost", "127.0.0.1", "::1"] {
entries.push(format_proxy_host_port(candidate, port));
}
} else {
entries.push(format_proxy_host_port(host, port));
}
entries
}
fn format_proxy_host_port(host: &str, port: i64) -> String {
if host.contains(':') {
format!("[{host}]:{port}")
} else {
format!("{host}:{port}")
}
}
fn is_loopback_host(host: &str) -> bool {
let host_lower = host.to_ascii_lowercase();
host_lower == "localhost" || host == "127.0.0.1" || host == "::1"
}
#[derive(Default)]
struct ProxyPorts {
http: Vec<u16>,
socks: Vec<u16>,
}
fn proxy_ports_from_env(env: &HashMap<String, String>) -> ProxyPorts {
let mut http_ports = BTreeSet::new();
let mut socks_ports = BTreeSet::new();
for key in PROXY_ENV_KEYS {
let Some(proxy_url) = env.get(*key) else {
continue;
};
let Some((host, port)) = network_proxy::proxy_host_port(proxy_url) else {
continue;
};
let Some(port) = normalize_proxy_port(port) else {
continue;
};
if !is_loopback_host(&host) {
continue;
}
let scheme = proxy_url_scheme(proxy_url).unwrap_or("http");
if scheme.to_ascii_lowercase().starts_with("socks") {
socks_ports.insert(port);
} else {
http_ports.insert(port);
}
}
ProxyPorts {
http: http_ports.into_iter().collect(),
socks: socks_ports.into_iter().collect(),
}
}
fn proxy_url_scheme(proxy_url: &str) -> Option<&str> {
proxy_url.split_once("://").map(|(scheme, _)| scheme)
}
fn normalize_proxy_port(port: i64) -> Option<u16> {
if (1..=u16::MAX as i64).contains(&port) {
Some(port as u16)
} else {
None
}
}
fn normalize_unix_socket_path(path: &str) -> Option<String> {
let trimmed = path.trim();
if trimmed.is_empty() {
return None;
}
let path_buf = PathBuf::from(trimmed);
let normalized = if path_buf.is_absolute() {
path_buf.canonicalize().unwrap_or(path_buf)
} else {
path_buf
};
Some(normalized.to_string_lossy().to_string())
}
fn escape_sbpl_string(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}
fn build_network_policy(
proxy_allowlist: &[String],
policy: &network_proxy::NetworkPolicy,
proxy_ports: &ProxyPorts,
) -> String {
let mut network_rules = String::from("; Network\n");
if proxy_allowlist.is_empty() {
network_rules.push_str("(allow network*)\n");
return format!("{network_rules}{MACOS_SEATBELT_NETWORK_POLICY_BASE}");
}
if policy.allow_local_binding {
network_rules.push_str("(allow network-bind (local ip \"localhost:*\"))\n");
network_rules.push_str("(allow network-inbound (local ip \"localhost:*\"))\n");
network_rules.push_str("(allow network-outbound (local ip \"localhost:*\"))\n");
}
if !policy.allow_unix_sockets.is_empty() {
let mut seen = HashSet::new();
for socket_path in &policy.allow_unix_sockets {
let Some(normalized) = normalize_unix_socket_path(socket_path) else {
continue;
};
if !seen.insert(normalized.clone()) {
continue;
}
let escaped = escape_sbpl_string(&normalized);
network_rules.push_str(&format!("(allow network* (subpath \"{escaped}\"))\n"));
}
}
for port in &proxy_ports.http {
network_rules.push_str(&format!(
"(allow network-bind (local ip \"localhost:{port}\"))\n"
));
network_rules.push_str(&format!(
"(allow network-inbound (local ip \"localhost:{port}\"))\n"
));
network_rules.push_str(&format!(
"(allow network-outbound (remote ip \"localhost:{port}\"))\n"
));
}
for port in &proxy_ports.socks {
network_rules.push_str(&format!(
"(allow network-bind (local ip \"localhost:{port}\"))\n"
));
network_rules.push_str(&format!(
"(allow network-inbound (local ip \"localhost:{port}\"))\n"
));
network_rules.push_str(&format!(
"(allow network-outbound (remote ip \"localhost:{port}\"))\n"
));
}
let mut outbound = String::from("(allow network-outbound\n");
for endpoint in proxy_allowlist {
outbound.push_str(&format!(" (remote tcp \"{endpoint}\")\n"));
}
outbound.push_str(")\n");
network_rules.push_str(&outbound);
format!("{network_rules}{MACOS_SEATBELT_NETWORK_POLICY_BASE}")
}
pub(crate) fn create_seatbelt_command_args(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
env: &HashMap<String, String>,
) -> Vec<String> {
let (file_write_policy, file_write_dir_params) = {
if sandbox_policy.has_full_disk_write_access() {
@@ -113,9 +307,15 @@ pub(crate) fn create_seatbelt_command_args(
// TODO(mbolin): apply_patch calls must also honor the SandboxPolicy.
let network_policy = if sandbox_policy.has_full_network_access() {
MACOS_SEATBELT_NETWORK_POLICY
let proxy_allowlist = proxy_allowlist_from_env(env);
let proxy_ports = proxy_ports_from_env(env);
let policy = config::default_config_path()
.ok()
.and_then(|path| network_proxy::load_network_policy(&path).ok())
.unwrap_or_default();
build_network_policy(&proxy_allowlist, &policy, &proxy_ports)
} else {
""
String::new()
};
let full_policy = format!(
@@ -168,17 +368,42 @@ mod tests {
use crate::protocol::SandboxPolicy;
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use pretty_assertions::assert_eq;
use serial_test::serial;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
struct CodexHomeGuard {
previous: Option<String>,
}
impl CodexHomeGuard {
fn new(path: &Path) -> Self {
let previous = std::env::var("CODEX_HOME").ok();
std::env::set_var("CODEX_HOME", path);
Self { previous }
}
}
impl Drop for CodexHomeGuard {
fn drop(&mut self) {
if let Some(previous) = self.previous.take() {
std::env::set_var("CODEX_HOME", previous);
} else {
std::env::remove_var("CODEX_HOME");
}
}
}
#[test]
#[serial]
fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
// Create a temporary workspace with two writable roots: one containing
// top-level .git and .codex directories and one without them.
let tmp = TempDir::new().expect("tempdir");
let _codex_home_guard = CodexHomeGuard::new(tmp.path());
let PopulatedTmp {
vulnerable_root,
vulnerable_root_canonical,
@@ -189,6 +414,7 @@ mod tests {
} = populate_tmpdir(tmp.path());
let cwd = tmp.path().join("cwd");
fs::create_dir_all(&cwd).expect("create cwd");
let env = std::collections::HashMap::new();
// Build a policy that only includes the two test roots as writable and
// does not automatically include defaults TMPDIR or /tmp.
@@ -217,7 +443,7 @@ mod tests {
.iter()
.map(std::string::ToString::to_string)
.collect();
let args = create_seatbelt_command_args(shell_command.clone(), &policy, &cwd);
let args = create_seatbelt_command_args(shell_command.clone(), &policy, &cwd, &env);
// Build the expected policy text using a raw string for readability.
// Note that the policy includes:
@@ -308,7 +534,8 @@ mod tests {
.iter()
.map(std::string::ToString::to_string)
.collect();
let write_hooks_file_args = create_seatbelt_command_args(shell_command_git, &policy, &cwd);
let write_hooks_file_args =
create_seatbelt_command_args(shell_command_git, &policy, &cwd, &env);
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
.args(&write_hooks_file_args)
.current_dir(&cwd)
@@ -345,7 +572,7 @@ mod tests {
.map(std::string::ToString::to_string)
.collect();
let write_allowed_file_args =
create_seatbelt_command_args(shell_command_allowed, &policy, &cwd);
create_seatbelt_command_args(shell_command_allowed, &policy, &cwd, &env);
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
.args(&write_allowed_file_args)
.current_dir(&cwd)
@@ -365,10 +592,12 @@ mod tests {
}
#[test]
#[serial]
fn create_seatbelt_args_for_cwd_as_git_repo() {
// Create a temporary workspace with two writable roots: one containing
// top-level .git and .codex directories and one without them.
let tmp = TempDir::new().expect("tempdir");
let _codex_home_guard = CodexHomeGuard::new(tmp.path());
let PopulatedTmp {
vulnerable_root,
vulnerable_root_canonical,
@@ -376,6 +605,7 @@ mod tests {
dot_codex_canonical,
..
} = populate_tmpdir(tmp.path());
let env = std::collections::HashMap::new();
// Build a policy that does not specify any writable_roots, but does
// use the default ones (cwd and TMPDIR) and verifies the `.git` and
@@ -400,8 +630,12 @@ mod tests {
.iter()
.map(std::string::ToString::to_string)
.collect();
let args =
create_seatbelt_command_args(shell_command.clone(), &policy, vulnerable_root.as_path());
let args = create_seatbelt_command_args(
shell_command.clone(),
&policy,
vulnerable_root.as_path(),
&env,
);
let tmpdir_env_var = std::env::var("TMPDIR")
.ok()
@@ -470,6 +704,41 @@ mod tests {
assert_eq!(expected_args, args);
}
#[test]
#[serial]
fn create_seatbelt_args_with_proxy_allowlist() {
let tmp = TempDir::new().expect("tempdir");
let _codex_home_guard = CodexHomeGuard::new(tmp.path());
let policy = SandboxPolicy::DangerFullAccess;
let cwd = std::env::current_dir().expect("getcwd");
let env = std::collections::HashMap::from([(
"HTTP_PROXY".to_string(),
"http://127.0.0.1:3128".to_string(),
)]);
let args = create_seatbelt_command_args(vec!["true".to_string()], &policy, &cwd, &env);
let policy_text = &args[1];
assert!(
policy_text.contains("(allow network-bind (local ip \"localhost:3128\"))"),
"expected seatbelt policy to allow local proxy binding"
);
assert!(
policy_text.contains("(allow network-inbound (local ip \"localhost:3128\"))"),
"expected seatbelt policy to allow local proxy inbound"
);
assert!(
policy_text.contains("(allow network-outbound (remote ip \"localhost:3128\"))"),
"expected seatbelt policy to allow local proxy outbound"
);
assert!(
policy_text.contains("(remote tcp \"127.0.0.1:3128\")"),
"expected seatbelt policy to include the proxy allowlist"
);
assert!(
!policy_text.contains("localhost:*"),
"proxy-restricted policy should not allow all localhost ports"
);
}
struct PopulatedTmp {
/// Path containing a .git and .codex subfolder.
/// For the purposes of this test, we consider this a "vulnerable" root
@@ -1,8 +1,7 @@
; when network access is enabled, these policies are added after those in seatbelt_base_policy.sbpl
; network allow rules are injected by codex-core based on proxy settings.
; Ref https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/network.sb;drc=f8f264d5e4e7509c913f4c60c2639d15905a07e4
(allow network-outbound)
(allow network-inbound)
(allow system-socket)
(allow mach-lookup
+2 -2
View File
@@ -15,7 +15,7 @@ use crate::exec::SandboxType;
use crate::exec::StdoutStream;
use crate::exec::StreamOutput;
use crate::exec::execute_exec_env;
use crate::exec_env::create_env_with_network_proxy;
use crate::exec_env::create_env;
use crate::parse_command::parse_command;
use crate::protocol::EventMsg;
use crate::protocol::ExecCommandBeginEvent;
@@ -97,7 +97,7 @@ impl SessionTask for UserShellCommandTask {
let exec_env = ExecEnv {
command: command.clone(),
cwd: cwd.clone(),
env: create_env_with_network_proxy(
env: create_env(
&turn_context.shell_environment_policy,
&sandbox_policy,
&turn_context.network_proxy,
+8 -4
View File
@@ -5,7 +5,7 @@ use std::sync::Arc;
use crate::codex::TurnContext;
use crate::exec::ExecParams;
use crate::exec_env::create_env_with_network_proxy;
use crate::exec_env::create_env;
use crate::exec_policy::create_exec_approval_requirement_for_command;
use crate::function_tool::FunctionCallError;
use crate::is_safe_command::is_known_safe_command;
@@ -37,7 +37,7 @@ impl ShellHandler {
cwd: turn_context.resolve_path(params.workdir.clone()),
expiration: params.timeout_ms.into(),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
env: create_env_with_network_proxy(
env: create_env(
&turn_context.shell_environment_policy,
&turn_context.sandbox_policy,
&turn_context.network_proxy,
@@ -67,7 +67,7 @@ impl ShellCommandHandler {
cwd: turn_context.resolve_path(params.workdir.clone()),
expiration: params.timeout_ms.into(),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
env: create_env_with_network_proxy(
env: create_env(
&turn_context.shell_environment_policy,
&turn_context.sandbox_policy,
&turn_context.network_proxy,
@@ -407,7 +407,11 @@ mod tests {
let expected_command = session.user_shell().derive_exec_args(&command, true);
let expected_cwd = turn_context.resolve_path(workdir.clone());
let expected_env = create_env(&turn_context.shell_environment_policy);
let expected_env = create_env(
&turn_context.shell_environment_policy,
&turn_context.sandbox_policy,
&turn_context.network_proxy,
);
let params = ShellCommandToolCallParams {
command,
@@ -114,14 +114,12 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
session
.request_command_approval(
turn,
crate::codex::CommandApprovalRequest {
call_id,
command: vec!["apply_patch".to_string()],
cwd,
reason: Some(reason),
proposed_execpolicy_amendment: None,
network_preflight_only: false,
},
call_id,
vec!["apply_patch".to_string()],
cwd,
Some(reason),
None,
false,
)
.await
} else if user_explicitly_approved {
+8 -11
View File
@@ -100,17 +100,14 @@ impl Approvable<ShellRequest> for ShellRuntime {
session
.request_command_approval(
turn,
crate::codex::CommandApprovalRequest {
call_id,
command,
cwd,
reason,
proposed_execpolicy_amendment: req
.exec_approval_requirement
.proposed_execpolicy_amendment()
.cloned(),
network_preflight_only: req.network_preflight_only,
},
call_id,
command,
cwd,
reason,
req.exec_approval_requirement
.proposed_execpolicy_amendment()
.cloned(),
req.network_preflight_only,
)
.await
})
@@ -120,17 +120,14 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
session
.request_command_approval(
turn,
crate::codex::CommandApprovalRequest {
call_id,
command,
cwd,
reason,
proposed_execpolicy_amendment: req
.exec_approval_requirement
.proposed_execpolicy_amendment()
.cloned(),
network_preflight_only: req.network_preflight_only,
},
call_id,
command,
cwd,
reason,
req.exec_approval_requirement
.proposed_execpolicy_amendment()
.cloned(),
req.network_preflight_only,
)
.await
})
@@ -13,7 +13,7 @@ use tokio_util::sync::CancellationToken;
use crate::bash::extract_bash_command;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::exec_env::create_env_with_network_proxy;
use crate::exec_env::create_env;
use crate::exec_policy::create_exec_approval_requirement_for_command;
use crate::network_proxy;
use crate::protocol::BackgroundEventEvent;
@@ -481,7 +481,7 @@ impl UnifiedExecSessionManager {
justification: Option<String>,
context: &UnifiedExecContext,
) -> Result<UnifiedExecSession, UnifiedExecError> {
let env = apply_unified_exec_env(create_env_with_network_proxy(
let env = apply_unified_exec_env(create_env(
&context.turn.shell_environment_policy,
&context.turn.sandbox_policy,
&context.turn.network_proxy,
+13 -12
View File
@@ -1,4 +1,5 @@
#![cfg(target_os = "linux")]
use codex_core::config::types::NetworkProxyConfig;
use codex_core::config::types::ShellEnvironmentPolicy;
use codex_core::error::CodexErr;
use codex_core::error::SandboxErr;
@@ -29,25 +30,16 @@ const NETWORK_TIMEOUT_MS: u64 = 2_000;
#[cfg(target_arch = "aarch64")]
const NETWORK_TIMEOUT_MS: u64 = 10_000;
fn create_env_from_core_vars() -> HashMap<String, String> {
fn create_env_from_core_vars(sandbox_policy: &SandboxPolicy) -> HashMap<String, String> {
let policy = ShellEnvironmentPolicy::default();
create_env(&policy)
let network_proxy = NetworkProxyConfig::default();
create_env(&policy, sandbox_policy, &network_proxy)
}
#[expect(clippy::print_stdout, clippy::expect_used, clippy::unwrap_used)]
async fn run_cmd(cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64) {
let cwd = std::env::current_dir().expect("cwd should exist");
let sandbox_cwd = cwd.clone();
let params = ExecParams {
command: cmd.iter().copied().map(str::to_owned).collect(),
cwd,
expiration: timeout_ms.into(),
env: create_env_from_core_vars(),
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
};
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: writable_roots
.iter()
@@ -60,6 +52,15 @@ async fn run_cmd(cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64) {
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let params = ExecParams {
command: cmd.iter().copied().map(str::to_owned).collect(),
cwd,
expiration: timeout_ms.into(),
env: create_env_from_core_vars(&sandbox_policy),
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
};
let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox");
let codex_linux_sandbox_exe = Some(PathBuf::from(sandbox_program));
let res = process_exec_tool_call(
+19 -5
View File
@@ -359,7 +359,17 @@ enabled = true
proxy_url = "http://127.0.0.1:3128"
admin_url = "http://127.0.0.1:8080"
mode = "limited" # limited | full (default)
no_proxy = ["localhost", "127.0.0.1"]
no_proxy = [
"localhost",
"127.0.0.1",
"::1",
"*.local",
".local",
"169.254.0.0/16",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
]
prompt_on_block = true
poll_interval_ms = 1000
```
@@ -367,8 +377,10 @@ poll_interval_ms = 1000
Notes:
- Proxy settings are injected only when sandbox network access is enabled (or full access mode). If the sandbox blocks network access, requests are blocked at the OS layer.
- `proxy_url` is used for `HTTP_PROXY`, `HTTPS_PROXY`, and `ALL_PROXY` env vars.
- `no_proxy` entries bypass the proxy; use sparingly because bypassed traffic is not filtered by the proxy policy.
- `proxy_url` is used for HTTP proxy envs (`HTTP_PROXY`, `HTTPS_PROXY`, `http_proxy`, `https_proxy`, plus npm/yarn variants). Docker and Cloud SDK proxy envs are derived from the HTTP proxy when present.
- When `proxy_url` points at localhost, Codex also assumes a SOCKS5 proxy on `localhost:8081` for `ALL_PROXY`, `GRPC_PROXY`, `FTP_PROXY`, `RSYNC_PROXY`, and (macOS only) `GIT_SSH_COMMAND`.
- `no_proxy` entries bypass the proxy; defaults include localhost + private network ranges. Use sparingly because bypassed traffic is not filtered by the proxy policy.
- `[network_proxy.policy]` can optionally allow localhost binding or Unix socket access (macOS only) when proxy-restricted network access is active.
- When `prompt_on_block = true`, Codex polls the proxy admin API (`/blocked`) and surfaces a prompt to allow for the session, allow always (add to allowlist), or deny (add to denylist). Allow/deny decisions update `~/.codex/config.toml` under `[network_proxy.policy]`, then Codex calls `/reload`.
### tools.\*
@@ -963,14 +975,16 @@ Valid values:
| `sandbox_workspace_write.exclude_tmpdir_env_var` | boolean | Exclude `$TMPDIR` from writable roots (default: false). |
| `sandbox_workspace_write.exclude_slash_tmp` | boolean | Exclude `/tmp` from writable roots (default: false). |
| `network_proxy.enabled` | boolean | Enable proxy environment injection + admin polling (default: false). |
| `network_proxy.proxy_url` | string | Proxy URL used for `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY` (default: `http://127.0.0.1:3128`). |
| `network_proxy.proxy_url` | string | Proxy URL used for HTTP proxy envs (`HTTP_PROXY`/`HTTPS_PROXY`); when it points at localhost, Codex also assumes SOCKS5 on `localhost:8081` for `ALL_PROXY` (default: `http://127.0.0.1:3128`). |
| `network_proxy.admin_url` | string | Proxy admin API base URL (default: `http://127.0.0.1:8080`). |
| `network_proxy.mode` | `limited` \| `full` | Default proxy mode for policy hints (default: `full`). |
| `network_proxy.no_proxy` | array<string> | Hosts/IPs that bypass the proxy (default includes `localhost`, `127.0.0.1`, `::1`). |
| `network_proxy.no_proxy` | array<string> | Hosts/IPs that bypass the proxy (default includes localhost + private network ranges). |
| `network_proxy.prompt_on_block` | boolean | Poll `/blocked` and prompt on denied requests (default: true). |
| `network_proxy.poll_interval_ms` | number | Admin poll interval in ms (default: 1000). |
| `network_proxy.policy.allowedDomains` | array<string> | Allowlist of domain patterns (denylist takes precedence). |
| `network_proxy.policy.deniedDomains` | array<string> | Denylist of domain patterns (takes precedence over allowlist). |
| `network_proxy.policy.allowLocalBinding` | boolean | Allow localhost binding when proxy-restricted (macOS only, default: false). |
| `network_proxy.policy.allowUnixSockets` | array<string> | Allow specific Unix socket paths when proxy-restricted (macOS only, default: []). |
| `network_proxy.mitm.enabled` | boolean | Enable HTTPS MITM for read-only enforcement in limited mode (default: false). |
| `network_proxy.mitm.inspect` | boolean | Enable body inspection in MITM mode (default: false). |
| `network_proxy.mitm.max_body_bytes` | number | Max body bytes to buffer when inspection is enabled (default: 4096). |
+22 -3
View File
@@ -106,19 +106,38 @@ exclude_slash_tmp = false
[network_proxy]
# Enable proxy env injection + approval prompts for blocked domains. Default: false
enabled = false
# HTTP/HTTPS/ALL proxy URL. Default: "http://127.0.0.1:3128"
# HTTP/HTTPS proxy URL. Default: "http://127.0.0.1:3128"
proxy_url = "http://127.0.0.1:3128"
# Admin API for the proxy (for /blocked, /reload, /mode). Default: "http://127.0.0.1:8080"
admin_url = "http://127.0.0.1:8080"
# limited | full (default: full)
mode = "full"
# Hosts/IPs that bypass the proxy. Default includes localhost + loopback.
no_proxy = ["localhost", "127.0.0.1", "::1"]
# Hosts/IPs that bypass the proxy. Default includes localhost + private networks.
no_proxy = [
"localhost",
"127.0.0.1",
"::1",
"*.local",
".local",
"169.254.0.0/16",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
]
# Poll proxy /blocked and prompt the user. Default: true
prompt_on_block = true
# Poll interval in milliseconds. Default: 1000
poll_interval_ms = 1000
[network_proxy.policy]
# Allow localhost binds inside the sandbox (macOS only). Default: false
allowLocalBinding = false
# Allow Unix socket paths inside the sandbox (macOS only). Default: []
allowUnixSockets = []
# Optional domain allow/deny lists (denylist wins)
allowedDomains = []
deniedDomains = []
################################################################################
# Shell Environment Policy for spawned processes
################################################################################
+3 -1
View File
@@ -76,7 +76,9 @@ Network access is controlled through a proxy server running outside the sandbox:
- **Custom proxy support:** Advanced users can implement custom rules on outgoing traffic.
- **Comprehensive coverage:** Restrictions apply to all scripts, programs, and subprocesses spawned by Codex.
`NO_PROXY` is supported via `[network_proxy].no_proxy` and is passed to subprocesses as `NO_PROXY/no_proxy`. Any domains or IPs in that list bypass the proxy and are not filtered by proxy policy.
`NO_PROXY` is supported via `[network_proxy].no_proxy` and is passed to subprocesses as `NO_PROXY/no_proxy`. Defaults include localhost and private network ranges; any entries in that list bypass the proxy and are not filtered by proxy policy.
On macOS, `[network_proxy.policy]` can also allow localhost binding or Unix socket paths when proxy-restricted network access is active. These settings influence the Seatbelt profile.
When MITM is enabled in the proxy config, Codex injects common CA environment variables (for example `SSL_CERT_FILE`, `CURL_CA_BUNDLE`, `GIT_SSL_CAINFO`, `REQUESTS_CA_BUNDLE`, `NODE_EXTRA_CA_CERTS`, `PIP_CERT`, and `NPM_CONFIG_CAFILE`) pointing at the proxy CA cert to reduce per‑tool configuration.