feat(sandbox): enforce proxy-aware network routing in sandbox (#11113)

## Summary
- expand proxy env injection to cover common tool env vars
(`HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY`/`NO_PROXY` families +
tool-specific variants)
- harden macOS Seatbelt network policy generation to route through
inferred loopback proxy endpoints and fail closed when proxy env is
malformed
- thread proxy-aware Linux sandbox flags and add minimal bwrap netns
isolation hook for restricted non-proxy runs
- add/refresh tests for proxy env wiring, Seatbelt policy generation,
and Linux sandbox argument wiring
This commit is contained in:
viyatb-oai
2026-02-09 23:44:21 -08:00
committed by GitHub
Unverified
parent b61ea47e83
commit 3391e5ea86
24 changed files with 1046 additions and 122 deletions
@@ -192,6 +192,7 @@ impl ToolHandler for UnifiedExecHandler {
yield_time_ms,
max_output_tokens,
workdir,
network: context.turn.config.network.clone(),
tty,
sandbox_permissions,
justification,
+12 -2
View File
@@ -3,7 +3,8 @@ Module: orchestrator
Central place for approvals + sandbox selection + retry semantics. Drives a
simple sequence for any ToolRuntime: approval → select sandbox → attempt →
retry without sandbox on denial (no reapproval thanks to caching).
retry with an escalated sandbox strategy on denial (no reapproval thanks to
caching).
*/
use crate::error::CodexErr;
use crate::error::SandboxErr;
@@ -87,12 +88,19 @@ impl ToolOrchestrator {
}
// 2) First attempt under the selected sandbox.
let has_managed_network_requirements = turn_ctx
.config
.config_layer_stack
.requirements_toml()
.network
.is_some();
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None,
SandboxOverride::NoOverride => self.sandbox.select_initial(
&turn_ctx.sandbox_policy,
tool.sandbox_preference(),
turn_ctx.windows_sandbox_level,
has_managed_network_requirements,
),
};
@@ -102,6 +110,7 @@ impl ToolOrchestrator {
let initial_attempt = SandboxAttempt {
sandbox: initial_sandbox,
policy: &turn_ctx.sandbox_policy,
enforce_managed_network: has_managed_network_requirements,
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(),
@@ -128,7 +137,7 @@ impl ToolOrchestrator {
})));
}
// Ask for approval before retrying without sandbox.
// Ask for approval before retrying with the escalated sandbox.
if !tool.should_bypass_approval(approval_policy, already_approved) {
let reason_msg = build_denial_reason_from_output(output.as_ref());
let approval_ctx = ApprovalCtx {
@@ -154,6 +163,7 @@ impl ToolOrchestrator {
let escalated_attempt = SandboxAttempt {
sandbox: crate::exec::SandboxType::None,
policy: &turn_ctx.sandbox_policy,
enforce_managed_network: has_managed_network_requirements,
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
codex_linux_sandbox_exe: None,
@@ -151,9 +151,9 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
) -> Result<ExecToolCallOutput, ToolError> {
let spec = Self::build_command_spec(req)?;
let env = attempt
.env_for(spec)
.env_for(spec, None)
.map_err(|err| ToolError::Codex(err.into()))?;
let out = execute_env(env, attempt.policy, None, Self::stdout_stream(ctx))
let out = execute_env(env, attempt.policy, Self::stdout_stream(ctx))
.await
.map_err(ToolError::Codex)?;
Ok(out)
+4 -9
View File
@@ -167,16 +167,11 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
req.justification.clone(),
)?;
let env = attempt
.env_for(spec)
.env_for(spec, req.network.as_ref())
.map_err(|err| ToolError::Codex(err.into()))?;
let out = execute_env(
env,
attempt.policy,
req.network.clone(),
Self::stdout_stream(ctx),
)
.await
.map_err(ToolError::Codex)?;
let out = execute_env(env, attempt.policy, Self::stdout_stream(ctx))
.await
.map_err(ToolError::Codex)?;
Ok(out)
}
}
@@ -2,7 +2,7 @@
Runtime: unified exec
Handles approval + sandbox orchestration for unified exec requests, delegating to
the process manager to spawn PTYs once an ExecEnv is prepared.
the process manager to spawn PTYs once an ExecRequest is prepared.
*/
use crate::error::CodexErr;
use crate::error::SandboxErr;
@@ -176,7 +176,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
)
.map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?;
let exec_env = attempt
.env_for(spec)
.env_for(spec, req.network.as_ref())
.map_err(|err| ToolError::Codex(err.into()))?;
self.manager
.open_session_with_exec_env(&exec_env, req.tty)
+6 -1
View File
@@ -12,6 +12,7 @@ use crate::sandboxing::CommandSpec;
use crate::sandboxing::SandboxManager;
use crate::sandboxing::SandboxTransformError;
use crate::state::SessionServices;
use codex_network_proxy::NetworkProxy;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ReviewDecision;
@@ -271,6 +272,7 @@ pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable {
pub(crate) struct SandboxAttempt<'a> {
pub sandbox: crate::exec::SandboxType,
pub policy: &'a crate::protocol::SandboxPolicy,
pub enforce_managed_network: bool,
pub(crate) manager: &'a SandboxManager,
pub(crate) sandbox_cwd: &'a Path,
pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
@@ -282,12 +284,15 @@ impl<'a> SandboxAttempt<'a> {
pub fn env_for(
&self,
spec: CommandSpec,
) -> Result<crate::sandboxing::ExecEnv, SandboxTransformError> {
network: Option<&NetworkProxy>,
) -> Result<crate::sandboxing::ExecRequest, SandboxTransformError> {
self.manager
.transform(crate::sandboxing::SandboxTransformRequest {
spec,
policy: self.policy,
sandbox: self.sandbox,
enforce_managed_network: self.enforce_managed_network,
network,
sandbox_policy_cwd: self.sandbox_cwd,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe,
use_linux_sandbox_bwrap: self.use_linux_sandbox_bwrap,