mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
remove major func signature changes
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user