mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Prepare managed network sandbox context (#29456)
## Why Managed network configures commands to use local HTTP and SOCKS proxies. For commands delegated to the exec server, the proxy environment and the sandbox policy were prepared separately. On macOS, that meant a command could receive `HTTPS_PROXY=http://127.0.0.1:43123` while Seatbelt still denied access to port `43123`. ## What changed `NetworkProxy` now prepares the command environment and sandbox context together from the same runtime snapshot: ```text Prepared managed network ├── command environment: HTTPS_PROXY=http://127.0.0.1:43123 └── sandbox context: allow outbound to 127.0.0.1:43123 ``` That context travels with remote exec requests. The exec server preserves the managed proxy and CA environment, and macOS Seatbelt allows only the prepared loopback proxy ports without enabling broad network access or local binding. The protocol field is optional and the existing enforcement flag remains in place, preserving compatibility with callers that do not send the new context.
This commit is contained in:
@@ -377,6 +377,7 @@ pub fn build_exec_request(
|
||||
args: args.to_vec(),
|
||||
cwd,
|
||||
env,
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
};
|
||||
let options = ExecOptions {
|
||||
@@ -459,6 +460,7 @@ pub(crate) async fn execute_exec_request(
|
||||
arg0,
|
||||
exec_server_sandbox: _,
|
||||
exec_server_enforce_managed_network: _,
|
||||
exec_server_managed_network: _,
|
||||
} = exec_request;
|
||||
|
||||
// TODO(anp): Keep PathUri through the local process launch boundary.
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::exec::execute_exec_request;
|
||||
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
@@ -63,6 +64,7 @@ pub struct ExecRequest {
|
||||
pub arg0: Option<String>,
|
||||
pub(crate) exec_server_sandbox: Option<FileSystemSandboxContext>,
|
||||
pub(crate) exec_server_enforce_managed_network: bool,
|
||||
pub(crate) exec_server_managed_network: Option<ManagedNetworkSandboxContext>,
|
||||
}
|
||||
|
||||
impl ExecRequest {
|
||||
@@ -107,6 +109,7 @@ impl ExecRequest {
|
||||
arg0,
|
||||
exec_server_sandbox: None,
|
||||
exec_server_enforce_managed_network: false,
|
||||
exec_server_managed_network: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +168,7 @@ impl ExecRequest {
|
||||
arg0,
|
||||
exec_server_sandbox: None,
|
||||
exec_server_enforce_managed_network: false,
|
||||
exec_server_managed_network: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +226,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
arg0: None,
|
||||
exec_server_sandbox: None,
|
||||
exec_server_enforce_managed_network: false,
|
||||
exec_server_managed_network: None,
|
||||
};
|
||||
|
||||
let stdout_stream = Some(StdoutStream {
|
||||
|
||||
@@ -50,6 +50,7 @@ pub(crate) fn build_sandbox_command(
|
||||
args: args.to_vec(),
|
||||
cwd,
|
||||
env: env.clone(),
|
||||
managed_network: None,
|
||||
additional_permissions,
|
||||
})
|
||||
}
|
||||
@@ -67,26 +68,26 @@ pub(crate) fn exec_env_for_sandbox_permissions(
|
||||
env
|
||||
}
|
||||
|
||||
pub(crate) fn strip_managed_proxy_env(env: &mut HashMap<String, String>) {
|
||||
for key in PROXY_ENV_KEYS {
|
||||
env.remove(*key);
|
||||
pub(crate) fn is_managed_proxy_env_var(key: &str, value: &str) -> bool {
|
||||
if PROXY_ENV_KEYS.contains(&key) {
|
||||
return true;
|
||||
}
|
||||
for key in CUSTOM_CA_ENV_KEYS {
|
||||
if env
|
||||
.get(key)
|
||||
.is_some_and(|value| is_managed_mitm_ca_trust_bundle_path(value))
|
||||
{
|
||||
env.remove(key);
|
||||
}
|
||||
if CUSTOM_CA_ENV_KEYS.contains(&key) {
|
||||
return is_managed_mitm_ca_trust_bundle_path(value);
|
||||
}
|
||||
// Only macOS injects a Codex-owned SSH wrapper for the managed SOCKS proxy.
|
||||
#[cfg(target_os = "macos")]
|
||||
if env
|
||||
.get(PROXY_GIT_SSH_COMMAND_ENV_KEY)
|
||||
.is_some_and(|command| command.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER))
|
||||
{
|
||||
env.remove(PROXY_GIT_SSH_COMMAND_ENV_KEY);
|
||||
key == PROXY_GIT_SSH_COMMAND_ENV_KEY
|
||||
&& value.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER)
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strip_managed_proxy_env(env: &mut HashMap<String, String>) {
|
||||
env.retain(|key, value| !is_managed_proxy_env_var(key, value));
|
||||
}
|
||||
|
||||
/// Prepends `path_entry` to `PATH`, removing duplicate and empty existing
|
||||
|
||||
@@ -168,6 +168,7 @@ pub(super) async fn try_run_zsh_fork(
|
||||
arg0,
|
||||
exec_server_sandbox: _,
|
||||
exec_server_enforce_managed_network: _,
|
||||
exec_server_managed_network: _,
|
||||
} = sandbox_exec_request;
|
||||
let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?;
|
||||
let effective_timeout = Duration::from_millis(
|
||||
@@ -902,6 +903,7 @@ impl CoreShellCommandExecutor {
|
||||
arg0: self.arg0.clone(),
|
||||
exec_server_sandbox: None,
|
||||
exec_server_enforce_managed_network: false,
|
||||
exec_server_managed_network: None,
|
||||
},
|
||||
/*stdout_stream*/ None,
|
||||
after_spawn,
|
||||
@@ -1010,6 +1012,7 @@ impl CoreShellCommandExecutor {
|
||||
args: args.to_vec(),
|
||||
cwd,
|
||||
env,
|
||||
managed_network: None,
|
||||
additional_permissions,
|
||||
};
|
||||
let options = ExecOptions {
|
||||
|
||||
@@ -41,6 +41,7 @@ use crate::unified_exec::NoopSpawnLifecycle;
|
||||
use crate::unified_exec::UnifiedExecError;
|
||||
use crate::unified_exec::UnifiedExecProcess;
|
||||
use crate::unified_exec::UnifiedExecProcessManager;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
@@ -117,6 +118,7 @@ fn build_unified_exec_sandbox_command(
|
||||
command: &[String],
|
||||
cwd: &PathUri,
|
||||
env: &HashMap<String, String>,
|
||||
managed_network: Option<ManagedNetworkSandboxContext>,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
) -> Result<SandboxCommand, ToolError> {
|
||||
let (program, args) = command
|
||||
@@ -127,6 +129,7 @@ fn build_unified_exec_sandbox_command(
|
||||
args: args.to_vec(),
|
||||
cwd: cwd.clone(),
|
||||
env: env.clone(),
|
||||
managed_network,
|
||||
additional_permissions,
|
||||
})
|
||||
}
|
||||
@@ -321,22 +324,28 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
req.network.as_ref(),
|
||||
launch_sandbox_permissions,
|
||||
);
|
||||
let mut env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions);
|
||||
if let Some(network) = managed_network {
|
||||
network
|
||||
.apply_to_env_for_optional_environment(
|
||||
&mut env,
|
||||
Some(&req.turn_environment.environment_id),
|
||||
)
|
||||
.map_err(|err| {
|
||||
ToolError::Codex(CodexErr::Io(io::Error::other(format!(
|
||||
"failed to prepare network proxy for environment `{}`: {err}",
|
||||
req.turn_environment.environment_id
|
||||
))))
|
||||
})?;
|
||||
}
|
||||
let env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions);
|
||||
let (env, managed_network_context) = match managed_network {
|
||||
Some(network) => {
|
||||
let prepared = network
|
||||
.prepare_for_optional_environment(
|
||||
env,
|
||||
Some(&req.turn_environment.environment_id),
|
||||
)
|
||||
.map_err(|err| {
|
||||
ToolError::Codex(CodexErr::Io(io::Error::other(format!(
|
||||
"failed to prepare network proxy for environment `{}`: {err}",
|
||||
req.turn_environment.environment_id
|
||||
))))
|
||||
})?;
|
||||
(prepared.env, Some(prepared.sandbox_context))
|
||||
}
|
||||
None => (env, None),
|
||||
};
|
||||
let explicit_env_overrides = req.explicit_env_overrides.clone();
|
||||
#[cfg(unix)]
|
||||
let mut env = env;
|
||||
#[cfg(unix)]
|
||||
let runtime_path_prepends = {
|
||||
let mut runtime_path_prepends = RuntimePathPrepends::default();
|
||||
if !environment_is_remote {
|
||||
@@ -385,6 +394,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
&command,
|
||||
&req.cwd,
|
||||
&env,
|
||||
managed_network_context.clone(),
|
||||
req.additional_permissions.clone(),
|
||||
)
|
||||
.map_err(|error| match error {
|
||||
@@ -450,6 +460,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
&command,
|
||||
&req.cwd,
|
||||
&env,
|
||||
managed_network_context,
|
||||
req.additional_permissions.clone(),
|
||||
)
|
||||
.map_err(|error| match error {
|
||||
|
||||
@@ -466,6 +466,7 @@ impl<'a> SandboxAttempt<'a> {
|
||||
network: Option<&NetworkProxy>,
|
||||
environment_id: Option<&str>,
|
||||
) -> Result<crate::sandboxing::ExecRequest, CodexErr> {
|
||||
let managed_network = command.managed_network.clone();
|
||||
let exec_server_permissions = effective_permission_profile(
|
||||
self.exec_server_permissions,
|
||||
command.additional_permissions.as_ref(),
|
||||
@@ -492,6 +493,7 @@ impl<'a> SandboxAttempt<'a> {
|
||||
options,
|
||||
self.workspace_roots.to_vec(),
|
||||
);
|
||||
exec_request.exec_server_managed_network = managed_network;
|
||||
if self.sandbox_requested {
|
||||
exec_request.exec_server_sandbox = Some(FileSystemSandboxContext {
|
||||
permissions: exec_server_permissions.into(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -212,7 +213,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(&cwd));
|
||||
let manager = SandboxManager::new();
|
||||
let attempt = SandboxAttempt {
|
||||
let mut attempt = SandboxAttempt {
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_requested: true,
|
||||
permissions: &permissions,
|
||||
@@ -227,20 +228,24 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
||||
windows_sandbox_private_desktop: false,
|
||||
network_denial_cancellation_token: None,
|
||||
};
|
||||
let command = SandboxCommand {
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
};
|
||||
let command = || SandboxCommand {
|
||||
program: "/bin/bash".into(),
|
||||
args: vec!["-lc".to_string(), "pwd".to_string()],
|
||||
cwd: cwd_uri.clone(),
|
||||
env: HashMap::new(),
|
||||
managed_network: Some(managed_network.clone()),
|
||||
additional_permissions: None,
|
||||
};
|
||||
let options = crate::sandboxing::ExecOptions {
|
||||
let options = || crate::sandboxing::ExecOptions {
|
||||
expiration: crate::exec::ExecExpiration::DefaultTimeout,
|
||||
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
|
||||
};
|
||||
|
||||
let request = attempt
|
||||
.env_for_exec_server(command, options, /*network*/ None, Some("remote"))
|
||||
.env_for_exec_server(command(), options(), /*network*/ None, Some("remote"))
|
||||
.expect("prepare remote exec request");
|
||||
|
||||
assert_eq!(
|
||||
@@ -256,8 +261,8 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
||||
assert_eq!(
|
||||
request.exec_server_sandbox,
|
||||
Some(codex_exec_server::FileSystemSandboxContext {
|
||||
permissions: exec_server_permissions.into(),
|
||||
cwd: Some(cwd_uri),
|
||||
permissions: exec_server_permissions.clone().into(),
|
||||
cwd: Some(cwd_uri.clone()),
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -265,4 +270,17 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
||||
})
|
||||
);
|
||||
assert!(request.exec_server_enforce_managed_network);
|
||||
assert_eq!(
|
||||
request.exec_server_managed_network,
|
||||
Some(managed_network.clone())
|
||||
);
|
||||
|
||||
attempt.sandbox_requested = false;
|
||||
let request = attempt
|
||||
.env_for_exec_server(command(), options(), /*network*/ None, Some("remote"))
|
||||
.expect("prepare unsandboxed remote exec request");
|
||||
|
||||
assert_eq!(request.exec_server_sandbox, None);
|
||||
assert!(!request.exec_server_enforce_managed_network);
|
||||
assert_eq!(request.exec_server_managed_network, Some(managed_network));
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::tools::events::ToolEventStage;
|
||||
use crate::tools::network_approval::DeferredNetworkApproval;
|
||||
use crate::tools::network_approval::finish_deferred_network_approval;
|
||||
use crate::tools::orchestrator::ToolOrchestrator;
|
||||
use crate::tools::runtimes::is_managed_proxy_env_var;
|
||||
use crate::tools::runtimes::unified_exec::UnifiedExecRequest as UnifiedExecToolRequest;
|
||||
use crate::tools::runtimes::unified_exec::UnifiedExecRuntime;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
@@ -143,10 +144,16 @@ fn exec_server_env_for_request(
|
||||
HashMap<String, String>,
|
||||
) {
|
||||
if let Some(exec_server_env_config) = &request.exec_server_env_config {
|
||||
(
|
||||
Some(exec_server_env_config.policy.clone()),
|
||||
env_overlay_for_exec_server(&request.env, &exec_server_env_config.local_policy_env),
|
||||
)
|
||||
let mut env =
|
||||
env_overlay_for_exec_server(&request.env, &exec_server_env_config.local_policy_env);
|
||||
if request.exec_server_managed_network.is_some() {
|
||||
for (key, value) in &request.env {
|
||||
if is_managed_proxy_env_var(key, value) {
|
||||
env.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(exec_server_env_config.policy.clone()), env)
|
||||
} else {
|
||||
(None, request.env.clone())
|
||||
}
|
||||
@@ -175,6 +182,7 @@ fn exec_server_params_for_request(
|
||||
arg0: request.arg0.clone(),
|
||||
sandbox: request.exec_server_sandbox.clone(),
|
||||
enforce_managed_network: request.exec_server_enforce_managed_network,
|
||||
managed_network: request.exec_server_managed_network.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::unified_exec::clamp_yield_time;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
@@ -76,6 +77,10 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy::unrestricted();
|
||||
let network_sandbox_policy = codex_protocol::permissions::NetworkSandboxPolicy::Restricted;
|
||||
let permission_profile = codex_protocol::models::PermissionProfile::Disabled;
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
};
|
||||
let mut request = ExecRequest {
|
||||
command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()],
|
||||
cwd: cwd.clone().into(),
|
||||
@@ -83,6 +88,15 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
("HOME".to_string(), "/client-home".to_string()),
|
||||
("PATH".to_string(), "/sandbox-path".to_string()),
|
||||
("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),
|
||||
(
|
||||
"HTTP_PROXY".to_string(),
|
||||
"http://127.0.0.1:43123".to_string(),
|
||||
),
|
||||
("CODEX_NETWORK_PROXY_ACTIVE".to_string(), "1".to_string()),
|
||||
(
|
||||
"SSL_CERT_FILE".to_string(),
|
||||
"/client/custom-ca.pem".to_string(),
|
||||
),
|
||||
]),
|
||||
exec_server_env_config: Some(ExecServerEnvConfig {
|
||||
policy: codex_exec_server::ExecEnvPolicy {
|
||||
@@ -95,6 +109,15 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
local_policy_env: HashMap::from([
|
||||
("HOME".to_string(), "/client-home".to_string()),
|
||||
("PATH".to_string(), "/client-path".to_string()),
|
||||
(
|
||||
"HTTP_PROXY".to_string(),
|
||||
"http://127.0.0.1:43123".to_string(),
|
||||
),
|
||||
("CODEX_NETWORK_PROXY_ACTIVE".to_string(), "1".to_string()),
|
||||
(
|
||||
"SSL_CERT_FILE".to_string(),
|
||||
"/client/custom-ca.pem".to_string(),
|
||||
),
|
||||
]),
|
||||
}),
|
||||
network: None,
|
||||
@@ -112,7 +135,8 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
windows_sandbox_filesystem_overrides: None,
|
||||
arg0: None,
|
||||
exec_server_sandbox: None,
|
||||
exec_server_enforce_managed_network: false,
|
||||
exec_server_enforce_managed_network: true,
|
||||
exec_server_managed_network: Some(managed_network.clone()),
|
||||
};
|
||||
|
||||
let params =
|
||||
@@ -120,12 +144,19 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
|
||||
assert_eq!(params.process_id.as_str(), "123");
|
||||
assert_eq!(params.cwd, request.cwd);
|
||||
assert!(params.enforce_managed_network);
|
||||
assert_eq!(params.managed_network, Some(managed_network));
|
||||
assert!(params.env_policy.is_some());
|
||||
assert_eq!(
|
||||
params.env,
|
||||
HashMap::from([
|
||||
("PATH".to_string(), "/sandbox-path".to_string()),
|
||||
("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),
|
||||
(
|
||||
"HTTP_PROXY".to_string(),
|
||||
"http://127.0.0.1:43123".to_string(),
|
||||
),
|
||||
("CODEX_NETWORK_PROXY_ACTIVE".to_string(), "1".to_string(),),
|
||||
])
|
||||
);
|
||||
request.exec_server_sandbox = Some(
|
||||
|
||||
Reference in New Issue
Block a user