Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/cli/src/debug_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ async fn run_command_under_windows_session(
windows_sandbox_level: WindowsSandboxLevel::from_config(config),
proxy_settings_mode: WindowsSandboxProxySettingsMode::Reconcile,
proxy_enforced: false,
network_proxy_restricting_sid: None,
timeout_ms: None,
read_roots_override: None,
read_roots_include_platform_defaults: false,
Expand Down
61 changes: 47 additions & 14 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1445,30 +1445,34 @@ async fn cli_main(
.await?;
}
Some(Subcommand::Sandbox(mut sandbox_cli)) => {
let config_profile = sandbox_cli
.config_profile
.as_ref()
.or(interactive.config_profile_v2.as_ref());
prepend_config_flags(
&mut sandbox_cli.config_overrides,
root_config_overrides.clone(),
);
#[cfg(target_os = "windows")]
if let Some(setup_cli) = sandbox_setup::parse_setup_command(&sandbox_cli.command)? {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"sandbox setup",
)?;
sandbox_setup::run(setup_cli).await?;
let cli_overrides = sandbox_cli
.config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
sandbox_setup::run(setup_cli, config_profile.cloned(), cli_overrides).await?;
return Ok(());
}
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"sandbox",
)?;
let config_profile = sandbox_cli
.config_profile
.as_ref()
.or(interactive.config_profile_v2.as_ref());
let loader_overrides = loader_overrides_for_profile(config_profile)?;
prepend_config_flags(
&mut sandbox_cli.config_overrides,
root_config_overrides.clone(),
);
#[cfg(target_os = "macos")]
codex_cli::run_command_under_seatbelt(
sandbox_cli,
Expand Down Expand Up @@ -1879,16 +1883,29 @@ fn loader_overrides_for_profile(
match profile_v2 {
Some(profile_v2) => {
let codex_home = find_codex_home()?;
Ok(LoaderOverrides {
user_config_path: Some(resolve_profile_v2_config_path(&codex_home, profile_v2)),
user_config_profile: Some(profile_v2.clone()),
..Default::default()
})
Ok(loader_overrides_for_profile_at_codex_home(
Some(profile_v2),
&codex_home,
))
}
None => Ok(LoaderOverrides::default()),
}
}

fn loader_overrides_for_profile_at_codex_home(
profile_v2: Option<&ProfileV2Name>,
codex_home: &std::path::Path,
) -> LoaderOverrides {
match profile_v2 {
Some(profile_v2) => LoaderOverrides {
user_config_path: Some(resolve_profile_v2_config_path(codex_home, profile_v2)),
user_config_profile: Some(profile_v2.clone()),
..Default::default()
},
None => LoaderOverrides::default(),
}
}

fn maybe_print_under_development_feature_warning(codex_home: &std::path::Path, feature: &str) {
let Some(spec) = FEATURES.iter().find(|spec| spec.key == feature) else {
return;
Expand Down Expand Up @@ -2706,6 +2723,22 @@ mod tests {
Ok(profile_v2_for_subcommand(&cli.interactive, subcommand)?.map(ToString::to_string))
}

#[test]
fn profile_loader_overrides_use_explicit_codex_home() -> anyhow::Result<()> {
let codex_home = tempfile::tempdir()?;
let profile: ProfileV2Name = "work".parse()?;

let overrides =
loader_overrides_for_profile_at_codex_home(Some(&profile), codex_home.path());

assert_eq!(
overrides.user_config_path,
Some(resolve_profile_v2_config_path(codex_home.path(), &profile))
);
assert_eq!(overrides.user_config_profile, Some(profile));
Ok(())
}

#[test]
fn profile_v2_is_rejected_for_config_management_subcommands() {
assert!(profile_v2_for_args(&["codex", "--profile", "work", "features", "list"]).is_err());
Expand Down
30 changes: 27 additions & 3 deletions codex-rs/cli/src/sandbox_setup.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::path::PathBuf;

use anyhow::Context;
use clap::ArgAction;
use clap::ArgGroup;
use clap::Parser;
use codex_core::config::ConfigBuilder;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::find_codex_home;
use codex_utils_cli::ProfileV2Name;
use toml::Value as TomlValue;

#[derive(Debug, Parser)]
#[command(group(
Expand Down Expand Up @@ -54,9 +58,13 @@ impl SandboxSetupCommand {
}
}

pub(crate) async fn run(cmd: SandboxSetupCommand) -> anyhow::Result<()> {
pub(crate) async fn run(
cmd: SandboxSetupCommand,
config_profile: Option<ProfileV2Name>,
cli_overrides: Vec<(String, TomlValue)>,
) -> anyhow::Result<()> {
match cmd.setup_level()? {
SandboxSetupLevel::Elevated => run_elevated(cmd).await,
SandboxSetupLevel::Elevated => run_elevated(cmd, config_profile, cli_overrides).await,
}
}

Expand All @@ -75,12 +83,28 @@ pub(crate) fn parse_setup_command(
.map_err(anyhow::Error::from)
}

async fn run_elevated(cmd: SandboxSetupCommand) -> anyhow::Result<()> {
async fn run_elevated(
cmd: SandboxSetupCommand,
config_profile: Option<ProfileV2Name>,
cli_overrides: Vec<(String, TomlValue)>,
) -> anyhow::Result<()> {
let identity = resolve_sandbox_setup_identity(&cmd)?;
let config = ConfigBuilder::default()
.codex_home(identity.codex_home.clone())
.fallback_cwd(Some(identity.codex_home.clone()))
.loader_overrides(super::loader_overrides_for_profile_at_codex_home(
config_profile.as_ref(),
&identity.codex_home,
))
.cli_overrides(cli_overrides)
.build()
.await
.context("failed to load target user's Codex config for sandbox provisioning")?;

codex_core::windows_sandbox::run_elevated_provisioning_setup(
identity.codex_home.as_path(),
identity.real_user.as_str(),
config.permissions.network.as_ref(),
)?;
ConfigEditsBuilder::new(identity.codex_home.as_path())
.set_windows_sandbox_mode("elevated")
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/core/src/config/network_proxy_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use codex_network_proxy::NetworkProxyHandle;
use codex_network_proxy::NetworkProxyState;
use codex_network_proxy::build_config_state;
use codex_network_proxy::host_and_port_from_network_addr;
#[cfg(any(target_os = "windows", test))]
use codex_network_proxy::managed_proxy_ports;
use codex_network_proxy::normalize_host;
use codex_network_proxy::validate_policy_against_constraints;
use codex_protocol::models::PermissionProfile;
Expand Down Expand Up @@ -85,6 +87,16 @@ impl NetworkProxySpec {
self.config.enable_socks5
}

#[cfg(any(target_os = "windows", test))]
pub(crate) fn configured_proxy_ports(&self) -> std::io::Result<Vec<u16>> {
managed_proxy_ports(&self.config).map_err(std::io::Error::other)
}

#[cfg(any(target_os = "windows", test))]
pub(crate) fn allow_local_binding(&self) -> bool {
self.config.allow_local_binding
}

pub(crate) fn from_config_and_constraints(
config: NetworkProxyConfig,
requirements: Option<NetworkConstraints>,
Expand Down
13 changes: 13 additions & 0 deletions codex-rs/core/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,18 @@ async fn exec_windows_sandbox(
network_proxy_environment_error(network_environment_id.as_deref(), err)
})?;
}
let network_proxy_restricting_sid = network
.as_ref()
.map(|network| {
network
.network_proxy_restricting_sid(network_environment_id.as_deref())
.ok_or_else(|| {
CodexErr::Io(io::Error::other(
"managed Windows proxy route is missing its restricting SID",
))
})
})
.transpose()?;

// Windows sandbox capture still receives timeout and cancellation separately.
let (cancellation, timeout_ms) = if capture_policy.uses_expiration() {
Expand Down Expand Up @@ -686,6 +698,7 @@ async fn exec_windows_sandbox(
cancellation,
use_private_desktop: windows_sandbox_private_desktop,
proxy_enforced,
network_proxy_restricting_sid,
read_roots_override: elevated_read_roots_override.as_deref(),
read_roots_include_platform_defaults:
elevated_read_roots_include_platform_defaults,
Expand Down
9 changes: 8 additions & 1 deletion codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,14 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
use tokio::io::AsyncReadExt as _;
use tokio::io::AsyncWriteExt as _;

let mut stream = tokio::net::TcpStream::connect(started_proxy.proxy().http_addr()).await?;
let prepared = started_proxy
.proxy()
.prepare_for_remote_environment(std::collections::HashMap::new(), "test-bridge")?;
let proxy_addr = prepared.env["HTTP_PROXY"]
.strip_prefix("http://")
.expect("HTTP proxy URL")
.parse::<std::net::SocketAddr>()?;
let mut stream = tokio::net::TcpStream::connect(proxy_addr).await?;
stream
.write_all(
b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n",
Expand Down
25 changes: 2 additions & 23 deletions codex-rs/core/src/tools/runtimes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use codex_network_proxy::PROXY_ACTIVE_ENV_KEY;
use codex_network_proxy::PROXY_ENV_KEYS;
#[cfg(target_os = "macos")]
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path;
pub(crate) use codex_network_proxy::is_managed_proxy_env_var;
pub(crate) use codex_network_proxy::strip_managed_proxy_env;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_sandboxing::SandboxCommand;
Expand Down Expand Up @@ -69,28 +70,6 @@ pub(crate) fn exec_env_for_sandbox_permissions(
env
}

pub(crate) fn is_managed_proxy_env_var(key: &str, value: &str) -> bool {
if PROXY_ENV_KEYS.contains(&key) {
return true;
}
if CUSTOM_CA_ENV_KEYS.contains(&key) {
return is_managed_mitm_ca_trust_bundle_path(value);
}
#[cfg(target_os = "macos")]
{
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
/// entries.
///
Expand Down
30 changes: 30 additions & 0 deletions codex-rs/core/src/unified_exec/process_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,35 @@ impl UnifiedExecProcessManager {
if request.command.is_empty() {
return Err(UnifiedExecError::MissingCommandLine);
}
let network_proxy_restricting_sid = {
#[cfg(target_os = "windows")]
{
if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken {
request
.network
.as_ref()
.map(|network| {
network
.network_proxy_restricting_sid(
request.network_environment_id.as_deref(),
)
.ok_or_else(|| {
UnifiedExecError::create_process(
"managed Windows proxy route is missing its restricting SID"
.to_string(),
)
})
})
.transpose()?
} else {
None
}
}
#[cfg(not(target_os = "windows"))]
{
None::<String>
}
};
let windows_sandbox = if request.sandbox
== codex_sandboxing::SandboxType::WindowsRestrictedToken
{
Expand All @@ -1039,6 +1068,7 @@ impl UnifiedExecProcessManager {
workspace_roots: &request.windows_sandbox_workspace_roots,
windows_sandbox_level: request.windows_sandbox_level,
proxy_enforced: request.network.is_some(),
network_proxy_restricting_sid: network_proxy_restricting_sid.as_deref(),
proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
filesystem_overrides: request.windows_sandbox_filesystem_overrides.as_ref(),
use_private_desktop: request.windows_sandbox_private_desktop,
Expand Down
31 changes: 28 additions & 3 deletions codex-rs/core/src/windows_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,30 @@ pub fn run_elevated_setup(
)
}

#[cfg(any(target_os = "windows", test))]
fn provisioning_settings(
network: Option<&crate::config::NetworkProxySpec>,
) -> std::io::Result<codex_windows_sandbox::WindowsSandboxProvisioningSettings> {
let Some(network) = network.filter(|network| network.enabled()) else {
return Ok(codex_windows_sandbox::WindowsSandboxProvisioningSettings::default());
};
Ok(codex_windows_sandbox::WindowsSandboxProvisioningSettings {
proxy_ports: network.configured_proxy_ports()?,
allow_local_binding: network.allow_local_binding(),
})
}

#[cfg(target_os = "windows")]
pub fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> anyhow::Result<()> {
codex_windows_sandbox::run_elevated_provisioning_setup(codex_home, real_user)
pub fn run_elevated_provisioning_setup(
codex_home: &Path,
real_user: &str,
network: Option<&crate::config::NetworkProxySpec>,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_elevated_provisioning_setup(
codex_home,
real_user,
provisioning_settings(network)?,
)
}

#[cfg(not(target_os = "windows"))]
Expand All @@ -140,7 +161,11 @@ pub fn run_elevated_setup(
}

#[cfg(not(target_os = "windows"))]
pub fn run_elevated_provisioning_setup(_codex_home: &Path, _real_user: &str) -> anyhow::Result<()> {
pub fn run_elevated_provisioning_setup(
_codex_home: &Path,
_real_user: &str,
_network: Option<&crate::config::NetworkProxySpec>,
) -> anyhow::Result<()> {
anyhow::bail!("elevated Windows sandbox setup is only supported on Windows")
}

Expand Down
Loading
Loading