mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fbbe7706d6
## Why This is the second PR in the Windows fs-helper sandbox stack. The fs-helper path needs a Windows sandbox launcher that has the same argv-shaped contract as macOS `sandbox-exec` and `codex-linux-sandbox`, but this PR only introduces that hidden launcher. It does not route fs-helper through it yet. The hidden launcher still needs to be policy-complete before later direct-spawn callers use it. In particular, it has to carry the same Windows sandbox policy details that the existing spawn paths already understand: proxy enforcement, read/write root overrides, and deny-read/deny-write overrides. ## What Changed - Added the hidden `codex.exe --run-as-windows-sandbox` arg1 dispatch path. - Added `windows-sandbox-rs/src/wrapper.rs`, which parses the wrapper argv, launches the requested command through the shared Windows sandbox session runner from PR1, and forwards stdio. - Added `create_windows_sandbox_command_args_for_permission_profile()` so later direct-spawn callers can build the wrapper argv consistently. - Made the wrapper argv round-trip the full Windows sandbox policy surface it needs later: workspace roots, environment, permission profile, sandbox level, private desktop, proxy enforcement, read/write root overrides, and deny-read/deny-write overrides. - Carried `proxy_enforced` through the shared Windows session request so proxy-managed executions continue to use the offline/elevated sandbox identity. - Added wrapper argument round-trip coverage for the full policy fields. ## Verification - `just test -p codex-windows-sandbox windows_wrapper_args_round_trip` - `just test -p codex-arg0` - `just test -p codex-core exec::tests::windows_` - `just fix -p codex-windows-sandbox -p codex-core -p codex-cli` Local note: the full `just fmt` command still fails on this workstation in non-Rust formatter setup (`uv` cache access denied and missing `dotslash`/buildifier), but the Rust formatter phase completed.
953 lines
31 KiB
Rust
953 lines
31 KiB
Rust
#[cfg(target_os = "macos")]
|
|
mod pid_tracker;
|
|
#[cfg(target_os = "macos")]
|
|
mod seatbelt;
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Stdio;
|
|
|
|
use codex_config::LoaderOverrides;
|
|
use codex_core::config::Config;
|
|
use codex_core::config::ConfigBuilder;
|
|
use codex_core::config::ConfigOverrides;
|
|
use codex_core::config::NetworkProxyAuditMetadata;
|
|
use codex_core::exec_env::create_env;
|
|
#[cfg(target_os = "macos")]
|
|
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
|
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
|
use codex_protocol::config_types::SandboxMode;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
use codex_sandboxing::landlock::allow_network_for_proxy;
|
|
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
|
#[cfg(target_os = "macos")]
|
|
use codex_sandboxing::seatbelt::CreateSeatbeltCommandArgsParams;
|
|
#[cfg(target_os = "macos")]
|
|
use codex_sandboxing::seatbelt::create_seatbelt_command_args;
|
|
use codex_sandboxing::with_managed_mitm_ca_readable_root;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_cli::CliConfigOverrides;
|
|
use tokio::process::Child;
|
|
use tokio::process::Command as TokioCommand;
|
|
use toml::Value as TomlValue;
|
|
|
|
use crate::LandlockCommand;
|
|
use crate::SeatbeltCommand;
|
|
use crate::WindowsCommand;
|
|
use crate::exit_status::handle_exit_status;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
use seatbelt::DenialLogger;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub async fn run_command_under_seatbelt(
|
|
command: SeatbeltCommand,
|
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
|
loader_overrides: LoaderOverrides,
|
|
) -> anyhow::Result<()> {
|
|
let SeatbeltCommand {
|
|
permissions_profile,
|
|
config_profile: _,
|
|
cwd,
|
|
include_managed_config,
|
|
allow_unix_sockets,
|
|
log_denials,
|
|
config_overrides,
|
|
command,
|
|
} = command;
|
|
let managed_requirements_mode = ManagedRequirementsMode::for_profile_invocation(
|
|
&permissions_profile,
|
|
include_managed_config,
|
|
);
|
|
run_command_under_sandbox(
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile,
|
|
cwd,
|
|
managed_requirements_mode,
|
|
loader_overrides,
|
|
},
|
|
command,
|
|
config_overrides,
|
|
codex_linux_sandbox_exe,
|
|
SandboxType::Seatbelt,
|
|
log_denials,
|
|
&allow_unix_sockets,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
pub async fn run_command_under_seatbelt(
|
|
_command: SeatbeltCommand,
|
|
_codex_linux_sandbox_exe: Option<PathBuf>,
|
|
_loader_overrides: LoaderOverrides,
|
|
) -> anyhow::Result<()> {
|
|
anyhow::bail!("Seatbelt sandbox is only available on macOS");
|
|
}
|
|
|
|
pub async fn run_command_under_landlock(
|
|
command: LandlockCommand,
|
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
|
loader_overrides: LoaderOverrides,
|
|
) -> anyhow::Result<()> {
|
|
let LandlockCommand {
|
|
permissions_profile,
|
|
config_profile: _,
|
|
cwd,
|
|
include_managed_config,
|
|
config_overrides,
|
|
command,
|
|
} = command;
|
|
let managed_requirements_mode = ManagedRequirementsMode::for_profile_invocation(
|
|
&permissions_profile,
|
|
include_managed_config,
|
|
);
|
|
run_command_under_sandbox(
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile,
|
|
cwd,
|
|
managed_requirements_mode,
|
|
loader_overrides,
|
|
},
|
|
command,
|
|
config_overrides,
|
|
codex_linux_sandbox_exe,
|
|
SandboxType::Landlock,
|
|
/*log_denials*/ false,
|
|
&[],
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn run_command_under_windows_sandbox(
|
|
command: WindowsCommand,
|
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
|
loader_overrides: LoaderOverrides,
|
|
) -> anyhow::Result<()> {
|
|
let WindowsCommand {
|
|
permissions_profile,
|
|
config_profile: _,
|
|
cwd,
|
|
include_managed_config,
|
|
config_overrides,
|
|
command,
|
|
} = command;
|
|
let managed_requirements_mode = ManagedRequirementsMode::for_profile_invocation(
|
|
&permissions_profile,
|
|
include_managed_config,
|
|
);
|
|
run_command_under_sandbox(
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile,
|
|
cwd,
|
|
managed_requirements_mode,
|
|
loader_overrides,
|
|
},
|
|
command,
|
|
config_overrides,
|
|
codex_linux_sandbox_exe,
|
|
SandboxType::Windows,
|
|
/*log_denials*/ false,
|
|
&[],
|
|
)
|
|
.await
|
|
}
|
|
|
|
enum SandboxType {
|
|
#[cfg(target_os = "macos")]
|
|
Seatbelt,
|
|
Landlock,
|
|
Windows,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct DebugSandboxConfigOptions {
|
|
permissions_profile: Option<String>,
|
|
cwd: Option<PathBuf>,
|
|
managed_requirements_mode: ManagedRequirementsMode,
|
|
loader_overrides: LoaderOverrides,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum ManagedRequirementsMode {
|
|
Include,
|
|
Ignore,
|
|
}
|
|
|
|
impl ManagedRequirementsMode {
|
|
fn for_profile_invocation(
|
|
permissions_profile: &Option<String>,
|
|
include_managed_config: bool,
|
|
) -> Self {
|
|
if permissions_profile.is_some() && !include_managed_config {
|
|
Self::Ignore
|
|
} else {
|
|
Self::Include
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn run_command_under_sandbox(
|
|
config_options: DebugSandboxConfigOptions,
|
|
command: Vec<String>,
|
|
config_overrides: CliConfigOverrides,
|
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
|
sandbox_type: SandboxType,
|
|
log_denials: bool,
|
|
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
|
|
allow_unix_sockets: &[AbsolutePathBuf],
|
|
) -> anyhow::Result<()> {
|
|
let config = load_debug_sandbox_config(
|
|
config_overrides
|
|
.parse_overrides()
|
|
.map_err(anyhow::Error::msg)?,
|
|
codex_linux_sandbox_exe,
|
|
config_options,
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
// In practice, this should be `std::env::current_dir()` because this CLI
|
|
// does not support `--cwd`, but let's use the config value for consistency.
|
|
let cwd = config.cwd.clone();
|
|
// Non-Windows sandbox launchers still use `sandbox_policy_cwd` for any
|
|
// remaining cwd-dependent policy resolution. `:workspace_roots` entries in
|
|
// the effective profile have already been materialized from config roots.
|
|
let sandbox_policy_cwd = cwd.clone();
|
|
#[cfg(target_os = "windows")]
|
|
let workspace_roots = config.effective_workspace_roots();
|
|
|
|
let env = create_env(
|
|
&config.permissions.shell_environment_policy,
|
|
/*thread_id*/ None,
|
|
);
|
|
|
|
// Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
|
|
if let SandboxType::Windows = sandbox_type {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
run_command_under_windows_session(&config, command, cwd, workspace_roots, env).await;
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
anyhow::bail!("Windows sandbox is only available on Windows");
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
let mut denial_logger = log_denials.then(DenialLogger::new).flatten();
|
|
#[cfg(not(target_os = "macos"))]
|
|
let _ = log_denials;
|
|
|
|
let managed_network_requirements_enabled = config.managed_network_requirements_enabled();
|
|
|
|
// This proxy should only live for the lifetime of the child process.
|
|
let network_proxy = match config.permissions.network.as_ref() {
|
|
Some(spec) => Some(
|
|
spec.start_proxy(
|
|
config.permissions.permission_profile(),
|
|
/*policy_decider*/ None,
|
|
/*blocked_request_observer*/ None,
|
|
managed_network_requirements_enabled,
|
|
NetworkProxyAuditMetadata::default(),
|
|
)
|
|
.await
|
|
.map_err(|err| anyhow::anyhow!("failed to start managed network proxy: {err}"))?,
|
|
),
|
|
None => None,
|
|
};
|
|
let network = network_proxy
|
|
.as_ref()
|
|
.map(codex_core::config::StartedNetworkProxy::proxy);
|
|
// Proxy containment depends on whether a proxy is active, not whether its
|
|
// policy came from managed requirements.
|
|
let enforce_managed_network = network.is_some();
|
|
let managed_mitm_ca_trust_bundle_path = match network.as_ref() {
|
|
Some(network) => network.managed_mitm_ca_trust_bundle_path(),
|
|
None => None,
|
|
};
|
|
let runtime_permission_profile = with_managed_mitm_ca_readable_root(
|
|
config.permissions.effective_permission_profile(),
|
|
managed_mitm_ca_trust_bundle_path.as_ref(),
|
|
sandbox_policy_cwd.as_path(),
|
|
);
|
|
|
|
let mut child = match sandbox_type {
|
|
#[cfg(target_os = "macos")]
|
|
SandboxType::Seatbelt => {
|
|
let (file_system_sandbox_policy, network_sandbox_policy) =
|
|
runtime_permission_profile.to_runtime_permissions();
|
|
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
|
command,
|
|
file_system_sandbox_policy: &file_system_sandbox_policy,
|
|
network_sandbox_policy,
|
|
sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
|
|
enforce_managed_network,
|
|
network: network.as_ref(),
|
|
extra_allow_unix_sockets: allow_unix_sockets,
|
|
});
|
|
spawn_debug_sandbox_child(
|
|
PathBuf::from("/usr/bin/sandbox-exec"),
|
|
args,
|
|
/*arg0*/ None,
|
|
cwd.to_path_buf(),
|
|
network_sandbox_policy,
|
|
env,
|
|
|env_map| {
|
|
env_map.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
|
if let Some(network) = network.as_ref() {
|
|
network.apply_to_env(env_map);
|
|
}
|
|
},
|
|
)
|
|
.await?
|
|
}
|
|
SandboxType::Landlock => {
|
|
#[expect(clippy::expect_used)]
|
|
let codex_linux_sandbox_exe = config
|
|
.codex_linux_sandbox_exe
|
|
.expect("codex-linux-sandbox executable not found");
|
|
let use_legacy_landlock = config.features.use_legacy_landlock();
|
|
let network_sandbox_policy = runtime_permission_profile.network_sandbox_policy();
|
|
let args = create_linux_sandbox_command_args_for_permission_profile(
|
|
command,
|
|
cwd.as_path(),
|
|
&runtime_permission_profile,
|
|
sandbox_policy_cwd.as_path(),
|
|
use_legacy_landlock,
|
|
allow_network_for_proxy(enforce_managed_network),
|
|
);
|
|
spawn_debug_sandbox_child(
|
|
codex_linux_sandbox_exe,
|
|
args,
|
|
Some("codex-linux-sandbox"),
|
|
cwd.to_path_buf(),
|
|
network_sandbox_policy,
|
|
env,
|
|
|env_map| {
|
|
if let Some(network) = network.as_ref() {
|
|
network.apply_to_env(env_map);
|
|
}
|
|
},
|
|
)
|
|
.await?
|
|
}
|
|
SandboxType::Windows => {
|
|
unreachable!("Windows sandbox should have been handled above");
|
|
}
|
|
};
|
|
|
|
#[cfg(target_os = "macos")]
|
|
if let Some(denial_logger) = &mut denial_logger {
|
|
denial_logger.on_child_spawn(&child);
|
|
}
|
|
|
|
let status = child.wait().await?;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
if let Some(denial_logger) = denial_logger {
|
|
let denials = denial_logger.finish().await;
|
|
eprintln!("\n=== Sandbox denials ===");
|
|
if denials.is_empty() {
|
|
eprintln!("None found.");
|
|
} else {
|
|
for seatbelt::SandboxDenial { name, capability } in denials {
|
|
eprintln!("({name}) {capability}");
|
|
}
|
|
}
|
|
}
|
|
|
|
handle_exit_status(status);
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
async fn run_command_under_windows_session(
|
|
config: &Config,
|
|
command: Vec<String>,
|
|
cwd: AbsolutePathBuf,
|
|
workspace_roots: Vec<AbsolutePathBuf>,
|
|
env: std::collections::HashMap<String, String>,
|
|
) -> ! {
|
|
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_windows_sandbox::WindowsSandboxSessionRequest;
|
|
use codex_windows_sandbox::spawn_windows_sandbox_session_for_level;
|
|
|
|
let permission_profile = config.permissions.effective_permission_profile();
|
|
let empty_paths: &[AbsolutePathBuf] = &[];
|
|
let spawned = spawn_windows_sandbox_session_for_level(WindowsSandboxSessionRequest {
|
|
permission_profile: &permission_profile,
|
|
workspace_roots: workspace_roots.as_slice(),
|
|
codex_home: config.codex_home.as_path(),
|
|
command,
|
|
cwd: cwd.as_path(),
|
|
env_map: env,
|
|
windows_sandbox_level: WindowsSandboxLevel::from_config(config),
|
|
proxy_enforced: false,
|
|
timeout_ms: None,
|
|
read_roots_override: None,
|
|
read_roots_include_platform_defaults: false,
|
|
write_roots_override: None,
|
|
deny_read_paths_override: empty_paths,
|
|
deny_write_paths_override: empty_paths,
|
|
tty: false,
|
|
stdin_open: true,
|
|
use_private_desktop: config.permissions.windows_sandbox_private_desktop,
|
|
})
|
|
.await;
|
|
|
|
let spawned = match spawned {
|
|
Ok(spawned) => spawned,
|
|
Err(err) => {
|
|
eprintln!("windows sandbox failed: {err}");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
let exit_code = codex_windows_sandbox::forward_sandbox_session_stdio(spawned).await;
|
|
std::process::exit(exit_code);
|
|
}
|
|
|
|
async fn spawn_debug_sandbox_child(
|
|
program: PathBuf,
|
|
args: Vec<String>,
|
|
arg0: Option<&str>,
|
|
cwd: PathBuf,
|
|
network_sandbox_policy: NetworkSandboxPolicy,
|
|
mut env: std::collections::HashMap<String, String>,
|
|
apply_env: impl FnOnce(&mut std::collections::HashMap<String, String>),
|
|
) -> std::io::Result<Child> {
|
|
let mut cmd = TokioCommand::new(&program);
|
|
#[cfg(unix)]
|
|
cmd.arg0(arg0.map_or_else(|| program.to_string_lossy().to_string(), String::from));
|
|
#[cfg(not(unix))]
|
|
let _ = arg0;
|
|
cmd.args(args);
|
|
cmd.current_dir(cwd);
|
|
apply_env(&mut env);
|
|
cmd.env_clear();
|
|
cmd.envs(env);
|
|
|
|
if !network_sandbox_policy.is_enabled() {
|
|
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
|
|
}
|
|
|
|
cmd.stdin(Stdio::inherit())
|
|
.stdout(Stdio::inherit())
|
|
.stderr(Stdio::inherit())
|
|
.kill_on_drop(true)
|
|
.spawn()
|
|
}
|
|
|
|
async fn load_debug_sandbox_config(
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
|
options: DebugSandboxConfigOptions,
|
|
strict_config: bool,
|
|
) -> anyhow::Result<Config> {
|
|
load_debug_sandbox_config_with_codex_home(
|
|
cli_overrides,
|
|
codex_linux_sandbox_exe,
|
|
options,
|
|
/*codex_home*/ None,
|
|
strict_config,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn load_debug_sandbox_config_with_codex_home(
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
|
options: DebugSandboxConfigOptions,
|
|
codex_home: Option<PathBuf>,
|
|
strict_config: bool,
|
|
) -> anyhow::Result<Config> {
|
|
let DebugSandboxConfigOptions {
|
|
permissions_profile,
|
|
cwd,
|
|
managed_requirements_mode,
|
|
loader_overrides,
|
|
} = options;
|
|
let mut cli_overrides = cli_overrides;
|
|
|
|
if let Some(permissions_profile) = permissions_profile {
|
|
cli_overrides.push((
|
|
"default_permissions".to_string(),
|
|
TomlValue::String(permissions_profile),
|
|
));
|
|
}
|
|
|
|
// For legacy configs, `codex sandbox` historically defaulted to read-only
|
|
// instead of inheriting ambient `sandbox_mode` settings from user/system
|
|
// config. Keep that behavior unless this invocation explicitly passes a
|
|
// legacy `sandbox_mode` CLI override for compatibility with older callers.
|
|
let uses_legacy_sandbox_mode_override = cli_overrides_use_legacy_sandbox_mode(&cli_overrides);
|
|
let config = build_debug_sandbox_config_with_loader_overrides(
|
|
cli_overrides.clone(),
|
|
ConfigOverrides {
|
|
cwd: cwd.clone(),
|
|
codex_linux_sandbox_exe: codex_linux_sandbox_exe.clone(),
|
|
..Default::default()
|
|
},
|
|
codex_home.clone(),
|
|
managed_requirements_mode,
|
|
loader_overrides.clone(),
|
|
strict_config,
|
|
)
|
|
.await?;
|
|
|
|
if config_uses_permission_profiles(&config) || uses_legacy_sandbox_mode_override {
|
|
return Ok(config);
|
|
}
|
|
|
|
build_debug_sandbox_config_with_loader_overrides(
|
|
cli_overrides,
|
|
ConfigOverrides {
|
|
sandbox_mode: Some(SandboxMode::ReadOnly),
|
|
cwd,
|
|
codex_linux_sandbox_exe,
|
|
..Default::default()
|
|
},
|
|
codex_home,
|
|
managed_requirements_mode,
|
|
loader_overrides,
|
|
strict_config,
|
|
)
|
|
.await
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
async fn build_debug_sandbox_config_with_loader_overrides(
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
harness_overrides: ConfigOverrides,
|
|
codex_home: Option<PathBuf>,
|
|
managed_requirements_mode: ManagedRequirementsMode,
|
|
mut loader_overrides: LoaderOverrides,
|
|
strict_config: bool,
|
|
) -> std::io::Result<Config> {
|
|
let mut builder = ConfigBuilder::default()
|
|
.cli_overrides(cli_overrides)
|
|
.harness_overrides(harness_overrides)
|
|
.strict_config(strict_config);
|
|
if matches!(managed_requirements_mode, ManagedRequirementsMode::Ignore) {
|
|
loader_overrides.ignore_managed_requirements = true;
|
|
}
|
|
builder = builder.loader_overrides(loader_overrides);
|
|
if let Some(codex_home) = codex_home {
|
|
builder = builder
|
|
.codex_home(codex_home.clone())
|
|
.fallback_cwd(Some(codex_home));
|
|
}
|
|
builder.build().await
|
|
}
|
|
|
|
fn config_uses_permission_profiles(config: &Config) -> bool {
|
|
config
|
|
.config_layer_stack
|
|
.effective_config()
|
|
.get("default_permissions")
|
|
.is_some()
|
|
}
|
|
|
|
fn cli_overrides_use_legacy_sandbox_mode(cli_overrides: &[(String, TomlValue)]) -> bool {
|
|
cli_overrides.iter().any(|(key, _)| key == "sandbox_mode")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
|
|
async fn build_debug_sandbox_config(
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
harness_overrides: ConfigOverrides,
|
|
codex_home: Option<PathBuf>,
|
|
managed_requirements_mode: ManagedRequirementsMode,
|
|
strict_config: bool,
|
|
) -> std::io::Result<Config> {
|
|
build_debug_sandbox_config_with_loader_overrides(
|
|
cli_overrides,
|
|
harness_overrides,
|
|
codex_home,
|
|
managed_requirements_mode,
|
|
LoaderOverrides::default(),
|
|
strict_config,
|
|
)
|
|
.await
|
|
}
|
|
|
|
fn escape_toml_path(path: &std::path::Path) -> String {
|
|
path.display().to_string().replace('\\', "\\\\")
|
|
}
|
|
|
|
fn write_permissions_profile_config(
|
|
codex_home: &TempDir,
|
|
docs: &std::path::Path,
|
|
private: &std::path::Path,
|
|
) -> std::io::Result<()> {
|
|
write_permissions_profile_config_to_path(
|
|
&codex_home.path().join("config.toml"),
|
|
docs,
|
|
private,
|
|
)
|
|
}
|
|
|
|
fn write_permissions_profile_config_to_path(
|
|
config_path: &std::path::Path,
|
|
docs: &std::path::Path,
|
|
private: &std::path::Path,
|
|
) -> std::io::Result<()> {
|
|
std::fs::create_dir_all(private)?;
|
|
let config = format!(
|
|
"default_permissions = \"limited-read-test\"\n\
|
|
[permissions.limited-read-test.filesystem]\n\
|
|
\":minimal\" = \"read\"\n\
|
|
\"{}\" = \"read\"\n\
|
|
\"{}\" = \"none\"\n\
|
|
\n\
|
|
[permissions.limited-read-test.network]\n\
|
|
enabled = true\n",
|
|
escape_toml_path(docs),
|
|
escape_toml_path(private),
|
|
);
|
|
std::fs::write(config_path, config)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn debug_sandbox_honors_active_permission_profiles() -> anyhow::Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
let sandbox_paths = TempDir::new()?;
|
|
let docs = sandbox_paths.path().join("docs");
|
|
let private = docs.join("private");
|
|
write_permissions_profile_config(&codex_home, &docs, &private)?;
|
|
let codex_home_path = codex_home.path().to_path_buf();
|
|
|
|
let profile_config = build_debug_sandbox_config(
|
|
Vec::new(),
|
|
ConfigOverrides::default(),
|
|
Some(codex_home_path.clone()),
|
|
ManagedRequirementsMode::Include,
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
let legacy_config = build_debug_sandbox_config(
|
|
Vec::new(),
|
|
ConfigOverrides {
|
|
sandbox_mode: Some(SandboxMode::ReadOnly),
|
|
..Default::default()
|
|
},
|
|
Some(codex_home_path.clone()),
|
|
ManagedRequirementsMode::Include,
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
let config = load_debug_sandbox_config_with_codex_home(
|
|
Vec::new(),
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile: None,
|
|
cwd: None,
|
|
managed_requirements_mode: ManagedRequirementsMode::Include,
|
|
loader_overrides: LoaderOverrides::default(),
|
|
},
|
|
Some(codex_home_path),
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
assert!(config_uses_permission_profiles(&config));
|
|
assert!(
|
|
profile_config.permissions.file_system_sandbox_policy()
|
|
!= legacy_config.permissions.file_system_sandbox_policy(),
|
|
"test fixture should distinguish profile syntax from legacy sandbox_mode"
|
|
);
|
|
assert_eq!(
|
|
config.permissions.file_system_sandbox_policy(),
|
|
profile_config.permissions.file_system_sandbox_policy(),
|
|
);
|
|
assert_ne!(
|
|
config.permissions.file_system_sandbox_policy(),
|
|
legacy_config.permissions.file_system_sandbox_policy(),
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn debug_sandbox_honors_config_profile_loader_overrides() -> anyhow::Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
let sandbox_paths = TempDir::new()?;
|
|
let docs = sandbox_paths.path().join("docs");
|
|
let private = docs.join("private");
|
|
let profile_path = codex_home.path().join("work.config.toml");
|
|
write_permissions_profile_config_to_path(&profile_path, &docs, &private)?;
|
|
let codex_home_path = codex_home.path().to_path_buf();
|
|
let loader_overrides = LoaderOverrides {
|
|
user_config_path: Some(AbsolutePathBuf::from_absolute_path(&profile_path)?),
|
|
user_config_profile: Some("work".parse().expect("profile name should parse")),
|
|
..LoaderOverrides::default()
|
|
};
|
|
|
|
let profile_config = build_debug_sandbox_config_with_loader_overrides(
|
|
Vec::new(),
|
|
ConfigOverrides::default(),
|
|
Some(codex_home_path.clone()),
|
|
ManagedRequirementsMode::Include,
|
|
loader_overrides.clone(),
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
let read_only_config = build_debug_sandbox_config(
|
|
Vec::new(),
|
|
ConfigOverrides {
|
|
sandbox_mode: Some(SandboxMode::ReadOnly),
|
|
..Default::default()
|
|
},
|
|
Some(codex_home_path.clone()),
|
|
ManagedRequirementsMode::Include,
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
let config = load_debug_sandbox_config_with_codex_home(
|
|
Vec::new(),
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile: None,
|
|
cwd: None,
|
|
managed_requirements_mode: ManagedRequirementsMode::Include,
|
|
loader_overrides,
|
|
},
|
|
Some(codex_home_path),
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
assert!(config_uses_permission_profiles(&config));
|
|
assert_ne!(
|
|
profile_config.permissions.file_system_sandbox_policy(),
|
|
read_only_config.permissions.file_system_sandbox_policy(),
|
|
"test fixture should distinguish the profile config from read-only"
|
|
);
|
|
assert_eq!(
|
|
config.permissions.file_system_sandbox_policy(),
|
|
profile_config.permissions.file_system_sandbox_policy(),
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn debug_sandbox_honors_explicit_legacy_sandbox_mode() -> anyhow::Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
let codex_home_path = codex_home.path().to_path_buf();
|
|
let cli_overrides = vec![(
|
|
"sandbox_mode".to_string(),
|
|
TomlValue::String("workspace-write".to_string()),
|
|
)];
|
|
|
|
let workspace_write_config = build_debug_sandbox_config(
|
|
cli_overrides.clone(),
|
|
ConfigOverrides::default(),
|
|
Some(codex_home_path.clone()),
|
|
ManagedRequirementsMode::Include,
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
let read_only_config = build_debug_sandbox_config(
|
|
Vec::new(),
|
|
ConfigOverrides {
|
|
sandbox_mode: Some(SandboxMode::ReadOnly),
|
|
..Default::default()
|
|
},
|
|
Some(codex_home_path.clone()),
|
|
ManagedRequirementsMode::Include,
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
let config = load_debug_sandbox_config_with_codex_home(
|
|
cli_overrides,
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile: None,
|
|
cwd: None,
|
|
managed_requirements_mode: ManagedRequirementsMode::Include,
|
|
loader_overrides: LoaderOverrides::default(),
|
|
},
|
|
Some(codex_home_path),
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
if cfg!(target_os = "windows") {
|
|
assert_eq!(
|
|
workspace_write_config
|
|
.permissions
|
|
.file_system_sandbox_policy(),
|
|
read_only_config.permissions.file_system_sandbox_policy(),
|
|
"workspace-write downgrades to read-only when the Windows sandbox is disabled"
|
|
);
|
|
} else {
|
|
assert_ne!(
|
|
workspace_write_config
|
|
.permissions
|
|
.file_system_sandbox_policy(),
|
|
read_only_config.permissions.file_system_sandbox_policy(),
|
|
"test fixture should distinguish explicit workspace-write from read-only"
|
|
);
|
|
}
|
|
assert_eq!(
|
|
config.permissions.file_system_sandbox_policy(),
|
|
workspace_write_config
|
|
.permissions
|
|
.file_system_sandbox_policy(),
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn debug_sandbox_defaults_legacy_configs_to_read_only() -> anyhow::Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
let codex_home_path = codex_home.path().to_path_buf();
|
|
|
|
let read_only_config = build_debug_sandbox_config(
|
|
Vec::new(),
|
|
ConfigOverrides {
|
|
sandbox_mode: Some(SandboxMode::ReadOnly),
|
|
..Default::default()
|
|
},
|
|
Some(codex_home_path.clone()),
|
|
ManagedRequirementsMode::Include,
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
let config = load_debug_sandbox_config_with_codex_home(
|
|
Vec::new(),
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile: None,
|
|
cwd: None,
|
|
managed_requirements_mode: ManagedRequirementsMode::Include,
|
|
loader_overrides: LoaderOverrides::default(),
|
|
},
|
|
Some(codex_home_path),
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
assert!(!config_uses_permission_profiles(&config));
|
|
assert_eq!(
|
|
config.permissions.file_system_sandbox_policy(),
|
|
read_only_config.permissions.file_system_sandbox_policy(),
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn debug_sandbox_honors_explicit_builtin_permission_profile() -> anyhow::Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
|
|
let config = load_debug_sandbox_config_with_codex_home(
|
|
Vec::new(),
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile: Some(":workspace".to_string()),
|
|
cwd: None,
|
|
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
|
loader_overrides: LoaderOverrides::default(),
|
|
},
|
|
Some(codex_home.path().to_path_buf()),
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
let actual = config
|
|
.permissions
|
|
.permission_profile()
|
|
.file_system_sandbox_policy();
|
|
let expected = codex_protocol::models::PermissionProfile::workspace_write()
|
|
.file_system_sandbox_policy();
|
|
assert!(
|
|
expected
|
|
.entries
|
|
.iter()
|
|
.all(|entry| actual.entries.contains(entry)),
|
|
"explicit workspace profile should preserve the built-in workspace rules"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn debug_sandbox_honors_explicit_named_permission_profile() -> anyhow::Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
let sandbox_paths = TempDir::new()?;
|
|
let docs = sandbox_paths.path().join("docs");
|
|
let private = docs.join("private");
|
|
write_permissions_profile_config(&codex_home, &docs, &private)?;
|
|
|
|
let config = load_debug_sandbox_config_with_codex_home(
|
|
Vec::new(),
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile: Some("limited-read-test".to_string()),
|
|
cwd: None,
|
|
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
|
loader_overrides: LoaderOverrides::default(),
|
|
},
|
|
Some(codex_home.path().to_path_buf()),
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
let expected = build_debug_sandbox_config(
|
|
vec![(
|
|
"default_permissions".to_string(),
|
|
TomlValue::String("limited-read-test".to_string()),
|
|
)],
|
|
ConfigOverrides::default(),
|
|
Some(codex_home.path().to_path_buf()),
|
|
ManagedRequirementsMode::Include,
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(
|
|
config.permissions.file_system_sandbox_policy(),
|
|
expected.permissions.file_system_sandbox_policy()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn debug_sandbox_uses_explicit_cwd() -> anyhow::Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
let cwd = TempDir::new()?;
|
|
|
|
let config = load_debug_sandbox_config_with_codex_home(
|
|
Vec::new(),
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
DebugSandboxConfigOptions {
|
|
permissions_profile: Some(":workspace".to_string()),
|
|
cwd: Some(cwd.path().to_path_buf()),
|
|
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
|
loader_overrides: LoaderOverrides::default(),
|
|
},
|
|
Some(codex_home.path().to_path_buf()),
|
|
/*strict_config*/ false,
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(config.cwd.as_path(), cwd.path());
|
|
|
|
Ok(())
|
|
}
|
|
}
|