mirror of
https://github.com/openai/codex.git
synced 2026-03-18 11:56:35 +03:00
Compare commits
1 Commits
latest-alp
...
dev/cc/zsh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c75b19076 |
5
codex-rs/Cargo.lock
generated
5
codex-rs/Cargo.lock
generated
@@ -1643,6 +1643,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"assert_matches",
|
||||
"async-trait",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"codex-app-server",
|
||||
@@ -1657,13 +1658,16 @@ dependencies = [
|
||||
"codex-execpolicy",
|
||||
"codex-login",
|
||||
"codex-mcp-server",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-responses-api-proxy",
|
||||
"codex-rmcp-client",
|
||||
"codex-shell-escalation",
|
||||
"codex-state",
|
||||
"codex-stdio-to-uds",
|
||||
"codex-tui",
|
||||
"codex-tui-app-server",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
"codex-windows-sandbox",
|
||||
@@ -1677,6 +1681,7 @@ dependencies = [
|
||||
"supports-color 3.0.2",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
|
||||
@@ -60,6 +60,7 @@ To test to see what happens when a command is run under the sandbox provided by
|
||||
```
|
||||
# macOS
|
||||
codex sandbox macos [--full-auto] [--log-denials] [COMMAND]...
|
||||
codex sandbox macos [--zsh-fork] [--log-denials] [COMMAND]...
|
||||
|
||||
# Linux
|
||||
codex sandbox linux [--full-auto] [COMMAND]...
|
||||
@@ -72,6 +73,8 @@ codex debug seatbelt [--full-auto] [--log-denials] [COMMAND]...
|
||||
codex debug landlock [--full-auto] [COMMAND]...
|
||||
```
|
||||
|
||||
Use `codex sandbox macos --zsh-fork` when you need to reproduce behavior from the zsh-fork exec-interception path. The command must be a `zsh -c ...` or `zsh -lc ...` invocation; intercepted child execs are then re-run under the same turn sandbox.
|
||||
|
||||
### Selecting a sandbox policy via `--sandbox`
|
||||
|
||||
The Rust CLI exposes a dedicated `--sandbox` (`-s`) flag that lets you pick the sandbox policy **without** having to reach for the generic `-c/--config` option:
|
||||
|
||||
@@ -16,6 +16,7 @@ path = "src/lib.rs"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
clap_complete = { workspace = true }
|
||||
@@ -32,13 +33,16 @@ codex-exec = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-mcp-server = { workspace = true }
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-responses-api-proxy = { workspace = true }
|
||||
codex-rmcp-client = { workspace = true }
|
||||
codex-shell-escalation = { workspace = true }
|
||||
codex-state = { workspace = true }
|
||||
codex-stdio-to-uds = { workspace = true }
|
||||
codex-tui = { workspace = true }
|
||||
codex-tui-app-server = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
owo-colors = { workspace = true }
|
||||
regex-lite = { workspace = true }
|
||||
@@ -52,6 +56,7 @@ tokio = { workspace = true, features = [
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
] }
|
||||
tokio-util = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
mod pid_tracker;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod seatbelt;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod zsh_fork;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
@@ -36,20 +38,26 @@ use seatbelt::DenialLogger;
|
||||
pub async fn run_command_under_seatbelt(
|
||||
command: SeatbeltCommand,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
main_execve_wrapper_exe: Option<PathBuf>,
|
||||
) -> anyhow::Result<()> {
|
||||
let SeatbeltCommand {
|
||||
full_auto,
|
||||
zsh_fork,
|
||||
log_denials,
|
||||
config_overrides,
|
||||
command,
|
||||
} = command;
|
||||
run_command_under_sandbox(
|
||||
full_auto,
|
||||
SandboxRunOptions {
|
||||
full_auto,
|
||||
zsh_fork,
|
||||
log_denials,
|
||||
},
|
||||
command,
|
||||
config_overrides,
|
||||
codex_linux_sandbox_exe,
|
||||
main_execve_wrapper_exe,
|
||||
SandboxType::Seatbelt,
|
||||
log_denials,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -58,6 +66,7 @@ pub async fn run_command_under_seatbelt(
|
||||
pub async fn run_command_under_seatbelt(
|
||||
_command: SeatbeltCommand,
|
||||
_codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
_main_execve_wrapper_exe: Option<PathBuf>,
|
||||
) -> anyhow::Result<()> {
|
||||
anyhow::bail!("Seatbelt sandbox is only available on macOS");
|
||||
}
|
||||
@@ -72,12 +81,16 @@ pub async fn run_command_under_landlock(
|
||||
command,
|
||||
} = command;
|
||||
run_command_under_sandbox(
|
||||
full_auto,
|
||||
SandboxRunOptions {
|
||||
full_auto,
|
||||
zsh_fork: false,
|
||||
log_denials: false,
|
||||
},
|
||||
command,
|
||||
config_overrides,
|
||||
codex_linux_sandbox_exe,
|
||||
None,
|
||||
SandboxType::Landlock,
|
||||
/*log_denials*/ false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -92,12 +105,16 @@ pub async fn run_command_under_windows(
|
||||
command,
|
||||
} = command;
|
||||
run_command_under_sandbox(
|
||||
full_auto,
|
||||
SandboxRunOptions {
|
||||
full_auto,
|
||||
zsh_fork: false,
|
||||
log_denials: false,
|
||||
},
|
||||
command,
|
||||
config_overrides,
|
||||
codex_linux_sandbox_exe,
|
||||
None,
|
||||
SandboxType::Windows,
|
||||
/*log_denials*/ false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -109,20 +126,27 @@ enum SandboxType {
|
||||
Windows,
|
||||
}
|
||||
|
||||
async fn run_command_under_sandbox(
|
||||
struct SandboxRunOptions {
|
||||
full_auto: bool,
|
||||
zsh_fork: bool,
|
||||
log_denials: bool,
|
||||
}
|
||||
|
||||
async fn run_command_under_sandbox(
|
||||
options: SandboxRunOptions,
|
||||
command: Vec<String>,
|
||||
config_overrides: CliConfigOverrides,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
main_execve_wrapper_exe: Option<PathBuf>,
|
||||
sandbox_type: SandboxType,
|
||||
log_denials: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let config = load_debug_sandbox_config(
|
||||
config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?,
|
||||
codex_linux_sandbox_exe,
|
||||
full_auto,
|
||||
main_execve_wrapper_exe,
|
||||
options.full_auto,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -139,6 +163,18 @@ async fn run_command_under_sandbox(
|
||||
/*thread_id*/ None,
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if matches!(sandbox_type, SandboxType::Seatbelt) && options.zsh_fork {
|
||||
return zsh_fork::run_command_under_zsh_fork(
|
||||
command,
|
||||
config,
|
||||
cwd,
|
||||
env,
|
||||
options.log_denials,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
|
||||
if let SandboxType::Windows = sandbox_type {
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -218,9 +254,9 @@ async fn run_command_under_sandbox(
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut denial_logger = log_denials.then(DenialLogger::new).flatten();
|
||||
let mut denial_logger = options.log_denials.then(DenialLogger::new).flatten();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = log_denials;
|
||||
let _ = options.log_denials;
|
||||
|
||||
let managed_network_requirements_enabled = config.managed_network_requirements_enabled();
|
||||
|
||||
@@ -374,11 +410,13 @@ async fn spawn_debug_sandbox_child(
|
||||
async fn load_debug_sandbox_config(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
main_execve_wrapper_exe: Option<PathBuf>,
|
||||
full_auto: bool,
|
||||
) -> anyhow::Result<Config> {
|
||||
load_debug_sandbox_config_with_codex_home(
|
||||
cli_overrides,
|
||||
codex_linux_sandbox_exe,
|
||||
main_execve_wrapper_exe,
|
||||
full_auto,
|
||||
/*codex_home*/ None,
|
||||
)
|
||||
@@ -388,6 +426,7 @@ async fn load_debug_sandbox_config(
|
||||
async fn load_debug_sandbox_config_with_codex_home(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
main_execve_wrapper_exe: Option<PathBuf>,
|
||||
full_auto: bool,
|
||||
codex_home: Option<PathBuf>,
|
||||
) -> anyhow::Result<Config> {
|
||||
@@ -395,6 +434,7 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
cli_overrides.clone(),
|
||||
ConfigOverrides {
|
||||
codex_linux_sandbox_exe: codex_linux_sandbox_exe.clone(),
|
||||
main_execve_wrapper_exe: main_execve_wrapper_exe.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.clone(),
|
||||
@@ -415,6 +455,7 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
ConfigOverrides {
|
||||
sandbox_mode: Some(create_sandbox_mode(full_auto)),
|
||||
codex_linux_sandbox_exe,
|
||||
main_execve_wrapper_exe,
|
||||
..Default::default()
|
||||
},
|
||||
codex_home,
|
||||
@@ -506,6 +547,7 @@ mod tests {
|
||||
let config = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
Some(codex_home_path),
|
||||
)
|
||||
@@ -540,6 +582,7 @@ mod tests {
|
||||
let err = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
)
|
||||
@@ -553,4 +596,23 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_threads_execve_wrapper_override() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let wrapper = codex_home.path().join("codex-execve-wrapper");
|
||||
|
||||
let config = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
None,
|
||||
Some(wrapper.clone()),
|
||||
false,
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.main_execve_wrapper_exe, Some(wrapper));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
409
codex-rs/cli/src/debug_sandbox/zsh_fork.rs
Normal file
409
codex-rs/cli/src/debug_sandbox/zsh_fork.rs
Normal file
@@ -0,0 +1,409 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::NetworkProxyAuditMetadata;
|
||||
use codex_core::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
||||
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_shell_escalation::EscalateServer;
|
||||
use codex_shell_escalation::EscalationDecision;
|
||||
use codex_shell_escalation::EscalationExecution;
|
||||
use codex_shell_escalation::EscalationPermissions;
|
||||
use codex_shell_escalation::EscalationPolicy;
|
||||
use codex_shell_escalation::ExecParams;
|
||||
use codex_shell_escalation::ExecResult;
|
||||
use codex_shell_escalation::PreparedExec;
|
||||
use codex_shell_escalation::ShellCommandExecutor;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use tokio::process::Child;
|
||||
use tokio::process::Command;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::seatbelt::DenialLogger;
|
||||
|
||||
pub(crate) async fn run_command_under_zsh_fork(
|
||||
command: Vec<String>,
|
||||
config: Config,
|
||||
cwd: PathBuf,
|
||||
env: HashMap<String, String>,
|
||||
log_denials: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let parsed = ParsedShellCommand::extract(&command)?;
|
||||
let shell_command = config
|
||||
.zsh_path
|
||||
.as_ref()
|
||||
.map(|zsh_path| {
|
||||
vec![
|
||||
zsh_path.to_string_lossy().to_string(),
|
||||
parsed.flag().to_string(),
|
||||
parsed.script.clone(),
|
||||
]
|
||||
})
|
||||
.unwrap_or(command);
|
||||
|
||||
let main_execve_wrapper_exe = config
|
||||
.main_execve_wrapper_exe
|
||||
.clone()
|
||||
.context("`codex sandbox macos --zsh-fork` requires main_execve_wrapper_exe")?;
|
||||
|
||||
let managed_network_requirements_enabled = config.managed_network_requirements_enabled();
|
||||
let network_proxy = match config.permissions.network.as_ref() {
|
||||
Some(spec) => Some(
|
||||
spec.start_proxy(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
/*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);
|
||||
|
||||
let denial_logger = Arc::new(Mutex::new(log_denials.then(DenialLogger::new).flatten()));
|
||||
let executor = DebugShellCommandExecutor {
|
||||
command: shell_command,
|
||||
cwd: cwd.clone(),
|
||||
env,
|
||||
file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: config.permissions.network_sandbox_policy,
|
||||
network,
|
||||
macos_seatbelt_profile_extensions: config
|
||||
.permissions
|
||||
.macos_seatbelt_profile_extensions
|
||||
.clone(),
|
||||
sandbox_policy_cwd: cwd,
|
||||
denial_logger: Arc::clone(&denial_logger),
|
||||
};
|
||||
let zsh_path = config.zsh_path.clone().unwrap_or_else(|| {
|
||||
PathBuf::from(
|
||||
executor
|
||||
.command
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "/bin/zsh".to_string()),
|
||||
)
|
||||
});
|
||||
let escalation_server = EscalateServer::new(
|
||||
zsh_path,
|
||||
main_execve_wrapper_exe,
|
||||
TurnDefaultEscalationPolicy,
|
||||
);
|
||||
let exec_result = escalation_server
|
||||
.exec(
|
||||
ExecParams {
|
||||
command: parsed.script,
|
||||
workdir: executor.cwd.to_string_lossy().to_string(),
|
||||
timeout_ms: None,
|
||||
login: Some(parsed.login),
|
||||
},
|
||||
CancellationToken::new(),
|
||||
Arc::new(executor),
|
||||
)
|
||||
.await?;
|
||||
|
||||
print_exec_result(&exec_result);
|
||||
|
||||
let denial_logger = denial_logger
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut logger| logger.take());
|
||||
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 denial in denials {
|
||||
eprintln!("({}) {}", denial.name, denial.capability);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::process::exit(exec_result.exit_code);
|
||||
}
|
||||
|
||||
struct ParsedShellCommand {
|
||||
script: String,
|
||||
login: bool,
|
||||
}
|
||||
|
||||
impl ParsedShellCommand {
|
||||
fn extract(command: &[String]) -> anyhow::Result<Self> {
|
||||
if let Some((login, script)) = command.windows(3).find_map(|parts| match parts {
|
||||
[_, flag, script] if flag == "-c" => Some((false, script.clone())),
|
||||
[_, flag, script] if flag == "-lc" => Some((true, script.clone())),
|
||||
_ => None,
|
||||
}) {
|
||||
return Ok(Self { script, login });
|
||||
}
|
||||
|
||||
anyhow::bail!(
|
||||
"`codex sandbox macos --zsh-fork` expects a `zsh -c ...` or `zsh -lc ...` command"
|
||||
)
|
||||
}
|
||||
|
||||
fn flag(&self) -> &'static str {
|
||||
if self.login { "-lc" } else { "-c" }
|
||||
}
|
||||
}
|
||||
|
||||
struct TurnDefaultEscalationPolicy;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EscalationPolicy for TurnDefaultEscalationPolicy {
|
||||
async fn determine_action(
|
||||
&self,
|
||||
program: &AbsolutePathBuf,
|
||||
argv: &[String],
|
||||
workdir: &AbsolutePathBuf,
|
||||
) -> anyhow::Result<EscalationDecision> {
|
||||
tracing::debug!("zsh-fork debug escalation for {program:?} {argv:?} in {workdir:?}");
|
||||
Ok(EscalationDecision::escalate(
|
||||
EscalationExecution::TurnDefault,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugShellCommandExecutor {
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
env: HashMap<String, String>,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
network: Option<NetworkProxy>,
|
||||
macos_seatbelt_profile_extensions:
|
||||
Option<codex_protocol::models::MacOsSeatbeltProfileExtensions>,
|
||||
sandbox_policy_cwd: PathBuf,
|
||||
denial_logger: Arc<Mutex<Option<DenialLogger>>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ShellCommandExecutor for DebugShellCommandExecutor {
|
||||
async fn run(
|
||||
&self,
|
||||
_command: Vec<String>,
|
||||
_cwd: PathBuf,
|
||||
env_overlay: HashMap<String, String>,
|
||||
_cancel_rx: CancellationToken,
|
||||
after_spawn: Option<Box<dyn FnOnce() + Send>>,
|
||||
) -> anyhow::Result<ExecResult> {
|
||||
let prepared = self.prepare_seatbelt_exec(
|
||||
self.command.clone(),
|
||||
self.cwd.clone(),
|
||||
overlay_env(&self.env, env_overlay),
|
||||
&self.file_system_sandbox_policy,
|
||||
self.network_sandbox_policy,
|
||||
self.macos_seatbelt_profile_extensions.as_ref(),
|
||||
);
|
||||
let child = spawn_prepared_command(&prepared)?;
|
||||
if let Ok(mut logger) = self.denial_logger.lock()
|
||||
&& let Some(denial_logger) = logger.as_mut()
|
||||
{
|
||||
denial_logger.on_child_spawn(&child);
|
||||
}
|
||||
if let Some(after_spawn) = after_spawn {
|
||||
after_spawn();
|
||||
}
|
||||
wait_for_output(child).await
|
||||
}
|
||||
|
||||
async fn prepare_escalated_exec(
|
||||
&self,
|
||||
program: &AbsolutePathBuf,
|
||||
argv: &[String],
|
||||
workdir: &AbsolutePathBuf,
|
||||
env: HashMap<String, String>,
|
||||
execution: EscalationExecution,
|
||||
) -> anyhow::Result<PreparedExec> {
|
||||
let command = join_program_and_argv(program, argv);
|
||||
let Some(first_arg) = argv.first() else {
|
||||
anyhow::bail!("intercepted exec request must contain argv[0]");
|
||||
};
|
||||
|
||||
match execution {
|
||||
EscalationExecution::TurnDefault => Ok(self.prepare_seatbelt_exec(
|
||||
command,
|
||||
workdir.to_path_buf(),
|
||||
env,
|
||||
&self.file_system_sandbox_policy,
|
||||
self.network_sandbox_policy,
|
||||
self.macos_seatbelt_profile_extensions.as_ref(),
|
||||
)),
|
||||
EscalationExecution::Unsandboxed => Ok(PreparedExec {
|
||||
command,
|
||||
cwd: workdir.to_path_buf(),
|
||||
env,
|
||||
arg0: Some(first_arg.clone()),
|
||||
}),
|
||||
EscalationExecution::Permissions(EscalationPermissions::Permissions(permissions)) => {
|
||||
Ok(self.prepare_seatbelt_exec(
|
||||
command,
|
||||
workdir.to_path_buf(),
|
||||
env,
|
||||
&permissions.file_system_sandbox_policy,
|
||||
permissions.network_sandbox_policy,
|
||||
permissions.macos_seatbelt_profile_extensions.as_ref(),
|
||||
))
|
||||
}
|
||||
EscalationExecution::Permissions(EscalationPermissions::PermissionProfile(_)) => {
|
||||
anyhow::bail!(
|
||||
"`codex sandbox macos --zsh-fork` does not yet support permission-profile escalations"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugShellCommandExecutor {
|
||||
fn prepare_seatbelt_exec(
|
||||
&self,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
mut env: HashMap<String, String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
macos_seatbelt_profile_extensions: Option<
|
||||
&codex_protocol::models::MacOsSeatbeltProfileExtensions,
|
||||
>,
|
||||
) -> PreparedExec {
|
||||
let args = create_seatbelt_command_args_for_policies_with_extensions(
|
||||
command,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
self.sandbox_policy_cwd.as_path(),
|
||||
false,
|
||||
self.network.as_ref(),
|
||||
macos_seatbelt_profile_extensions,
|
||||
);
|
||||
|
||||
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||||
if let Some(network) = self.network.as_ref() {
|
||||
network.apply_to_env(&mut env);
|
||||
}
|
||||
if !network_sandbox_policy.is_enabled() {
|
||||
env.insert(
|
||||
CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(),
|
||||
"1".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
PreparedExec {
|
||||
command: std::iter::once("/usr/bin/sandbox-exec".to_string())
|
||||
.chain(args)
|
||||
.collect(),
|
||||
cwd,
|
||||
env,
|
||||
arg0: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay_env(
|
||||
base_env: &HashMap<String, String>,
|
||||
env_overlay: HashMap<String, String>,
|
||||
) -> HashMap<String, String> {
|
||||
let mut env = base_env.clone();
|
||||
for (key, value) in env_overlay {
|
||||
env.insert(key, value);
|
||||
}
|
||||
env
|
||||
}
|
||||
|
||||
fn spawn_prepared_command(prepared: &PreparedExec) -> std::io::Result<Child> {
|
||||
let (program, args) = prepared.command.split_first().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"prepared command must not be empty",
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut command = Command::new(program);
|
||||
command
|
||||
.args(args)
|
||||
.arg0(prepared.arg0.clone().unwrap_or_else(|| program.to_string()))
|
||||
.env_clear()
|
||||
.envs(prepared.env.clone())
|
||||
.current_dir(&prepared.cwd)
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
}
|
||||
|
||||
async fn wait_for_output(child: Child) -> anyhow::Result<ExecResult> {
|
||||
let output = child.wait_with_output().await?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
|
||||
let exit_code = output.status.code().unwrap_or(1);
|
||||
|
||||
Ok(ExecResult {
|
||||
exit_code,
|
||||
output: format!("{stdout}{stderr}"),
|
||||
stdout,
|
||||
stderr,
|
||||
duration: Default::default(),
|
||||
timed_out: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn print_exec_result(exec_result: &ExecResult) {
|
||||
if !exec_result.stdout.is_empty() {
|
||||
print!("{}", exec_result.stdout);
|
||||
}
|
||||
if !exec_result.stderr.is_empty() {
|
||||
eprint!("{}", exec_result.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
fn join_program_and_argv(program: &AbsolutePathBuf, argv: &[String]) -> Vec<String> {
|
||||
std::iter::once(program.to_string_lossy().to_string())
|
||||
.chain(argv.iter().skip(1).cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ParsedShellCommand;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn extract_accepts_non_login_zsh_command() {
|
||||
let parsed = ParsedShellCommand::extract(&[
|
||||
"/bin/zsh".to_string(),
|
||||
"-c".to_string(),
|
||||
"echo hi".to_string(),
|
||||
])
|
||||
.expect("parse zsh command");
|
||||
|
||||
assert_eq!(parsed.script, "echo hi");
|
||||
assert!(!parsed.login);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_accepts_login_zsh_command() {
|
||||
let parsed = ParsedShellCommand::extract(&[
|
||||
"/bin/zsh".to_string(),
|
||||
"-lc".to_string(),
|
||||
"echo hi".to_string(),
|
||||
])
|
||||
.expect("parse zsh command");
|
||||
|
||||
assert_eq!(parsed.script, "echo hi");
|
||||
assert!(parsed.login);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@ pub struct SeatbeltCommand {
|
||||
#[arg(long = "full-auto", default_value_t = false)]
|
||||
pub full_auto: bool,
|
||||
|
||||
/// Route intercepted child execs through the zsh-fork execve wrapper using the turn sandbox.
|
||||
#[arg(long = "zsh-fork", default_value_t = false)]
|
||||
pub zsh_fork: bool,
|
||||
|
||||
/// While the command runs, capture macOS sandbox denials via `log stream` and print them after exit
|
||||
#[arg(long = "log-denials", default_value_t = false)]
|
||||
pub log_denials: bool,
|
||||
|
||||
@@ -795,6 +795,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
codex_cli::debug_sandbox::run_command_under_seatbelt(
|
||||
seatbelt_cli,
|
||||
arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
arg0_paths.main_execve_wrapper_exe.clone(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user