mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
Address exec-server sandbox review feedback
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -84,7 +84,10 @@ pub struct UnifiedExecRuntime<'a> {
|
||||
shell_mode: UnifiedExecShellMode,
|
||||
}
|
||||
|
||||
fn build_remote_exec_sandbox_config(attempt: &SandboxAttempt<'_>) -> Option<SandboxLaunchConfig> {
|
||||
fn build_remote_exec_sandbox_config(
|
||||
attempt: &SandboxAttempt<'_>,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
) -> Option<SandboxLaunchConfig> {
|
||||
if matches!(attempt.sandbox, codex_sandboxing::SandboxType::None) {
|
||||
return None;
|
||||
}
|
||||
@@ -95,6 +98,7 @@ fn build_remote_exec_sandbox_config(attempt: &SandboxAttempt<'_>) -> Option<Sand
|
||||
file_system_policy: attempt.file_system_policy.clone(),
|
||||
network_policy: attempt.network_policy,
|
||||
sandbox_policy_cwd: attempt.sandbox_cwd.to_path_buf(),
|
||||
additional_permissions,
|
||||
enforce_managed_network: attempt.enforce_managed_network,
|
||||
windows_sandbox_level: attempt.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
|
||||
@@ -102,13 +106,6 @@ fn build_remote_exec_sandbox_config(attempt: &SandboxAttempt<'_>) -> Option<Sand
|
||||
})
|
||||
}
|
||||
|
||||
fn should_remote_exec_server_build_sandbox(
|
||||
has_additional_permissions: bool,
|
||||
has_network_proxy: bool,
|
||||
) -> bool {
|
||||
!has_additional_permissions && !has_network_proxy
|
||||
}
|
||||
|
||||
impl<'a> UnifiedExecRuntime<'a> {
|
||||
/// Creates a runtime bound to the shared unified-exec process manager.
|
||||
pub fn new(manager: &'a UnifiedExecProcessManager, shell_mode: UnifiedExecShellMode) -> Self {
|
||||
@@ -245,36 +242,40 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
if let Some(network) = req.network.as_ref() {
|
||||
network.apply_to_env(&mut env);
|
||||
}
|
||||
// Remote exec-server now owns sandbox argv construction, so this branch
|
||||
// keeps sending raw command data until we collapse the launch APIs.
|
||||
if ctx.turn.environment.exec_server_url().is_some()
|
||||
&& should_remote_exec_server_build_sandbox(
|
||||
req.additional_permissions.is_some(),
|
||||
req.network.is_some(),
|
||||
)
|
||||
{
|
||||
let exec_params = codex_exec_server::ExecParams {
|
||||
process_id: req.process_id.to_string().into(),
|
||||
argv: command,
|
||||
cwd: req.cwd.clone(),
|
||||
env,
|
||||
tty: req.tty,
|
||||
arg0: None,
|
||||
sandbox: build_remote_exec_sandbox_config(attempt),
|
||||
};
|
||||
return self
|
||||
.manager
|
||||
.open_session_with_remote_exec(exec_params, ctx.turn.environment.as_ref())
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
UnifiedExecError::SandboxDenied { output, .. } => {
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output: Box::new(output),
|
||||
network_policy_decision: None,
|
||||
}))
|
||||
}
|
||||
other => ToolError::Rejected(other.to_string()),
|
||||
});
|
||||
if ctx.turn.environment.exec_server_url().is_some() {
|
||||
if let UnifiedExecShellMode::ZshFork(_) = &self.shell_mode {
|
||||
return Err(ToolError::Rejected(
|
||||
"unified_exec zsh-fork is not supported when exec_server_url is configured"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if req.network.is_none() {
|
||||
let exec_params = codex_exec_server::ExecParams {
|
||||
process_id: req.process_id.to_string().into(),
|
||||
argv: command,
|
||||
cwd: req.cwd.clone(),
|
||||
env,
|
||||
tty: req.tty,
|
||||
arg0: None,
|
||||
sandbox: build_remote_exec_sandbox_config(
|
||||
attempt,
|
||||
req.additional_permissions.clone(),
|
||||
),
|
||||
};
|
||||
return self
|
||||
.manager
|
||||
.open_session_with_remote_exec(exec_params, ctx.turn.environment.as_ref())
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
UnifiedExecError::SandboxDenied { output, .. } => {
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output: Box::new(output),
|
||||
network_policy_decision: None,
|
||||
}))
|
||||
}
|
||||
other => ToolError::Rejected(other.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {
|
||||
let command =
|
||||
@@ -297,14 +298,6 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.await?
|
||||
{
|
||||
Some(prepared) => {
|
||||
// TODO: Move unified-exec zsh-fork into exec-server once
|
||||
// remote launch can participate in the existing approval
|
||||
// and escalation flow.
|
||||
if ctx.turn.environment.exec_server_url().is_some() {
|
||||
return Err(ToolError::Rejected(
|
||||
"unified_exec zsh-fork is not supported when exec_server_url is configured".to_string(),
|
||||
));
|
||||
}
|
||||
return self
|
||||
.manager
|
||||
.open_session_with_exec_env(
|
||||
@@ -362,29 +355,3 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::should_remote_exec_server_build_sandbox;
|
||||
|
||||
#[test]
|
||||
fn remote_exec_server_builds_sandbox_for_simple_requests() {
|
||||
assert!(should_remote_exec_server_build_sandbox(
|
||||
/*has_additional_permissions*/ false, /*has_network_proxy*/ false,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_exec_server_falls_back_for_requests_with_additional_permissions() {
|
||||
assert!(!should_remote_exec_server_build_sandbox(
|
||||
/*has_additional_permissions*/ true, /*has_network_proxy*/ false,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_exec_server_falls_back_for_requests_with_network_proxy() {
|
||||
assert!(!should_remote_exec_server_build_sandbox(
|
||||
/*has_additional_permissions*/ false, /*has_network_proxy*/ true,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -947,15 +947,13 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<(
|
||||
|
||||
let open_call_id = "uexec-delayed-open";
|
||||
let open_args = json!({
|
||||
"cmd": "sleep 2 && echo MARKER1 && sleep 5 && echo MARKER2",
|
||||
"cmd": "sleep 3 && echo MARKER1 && sleep 3 && echo MARKER2",
|
||||
"yield_time_ms": 10,
|
||||
"tty": true,
|
||||
});
|
||||
|
||||
// Poll stdin three times: first for no output, second after the first marker,
|
||||
// and a final long poll to capture the second marker. Keep a wider buffer
|
||||
// between MARKER1 and process exit so slower environments still reach the
|
||||
// third write while the session is live.
|
||||
// and a final long poll to capture the second marker.
|
||||
let first_poll_call_id = "uexec-delayed-poll-1";
|
||||
let first_poll_args = json!({
|
||||
"chars": "x",
|
||||
@@ -967,7 +965,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<(
|
||||
let second_poll_args = json!({
|
||||
"chars": "x",
|
||||
"session_id": 1000,
|
||||
"yield_time_ms": 3500,
|
||||
"yield_time_ms": 4000,
|
||||
});
|
||||
|
||||
let third_poll_call_id = "uexec-delayed-poll-3";
|
||||
|
||||
@@ -15,10 +15,12 @@ path = "src/bin/codex-exec-server.rs"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-sandboxing = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use clap::Parser;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_arg0::arg0_dispatch_or_else;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct ExecServerArgs {
|
||||
@@ -11,8 +13,11 @@ struct ExecServerArgs {
|
||||
listen: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let args = ExecServerArgs::parse();
|
||||
codex_exec_server::run_main_with_listen_url(&args.listen).await
|
||||
fn main() -> anyhow::Result<()> {
|
||||
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
|
||||
let args = ExecServerArgs::parse();
|
||||
codex_exec_server::configure_arg0_paths(arg0_paths);
|
||||
codex_exec_server::run_main_with_listen_url(&args.listen).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ mod remote_process;
|
||||
mod rpc;
|
||||
mod server;
|
||||
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
|
||||
pub use client::ExecServerClient;
|
||||
pub use client::ExecServerError;
|
||||
pub use client_api::ExecServerClientConnectOptions;
|
||||
@@ -65,3 +67,7 @@ pub use server::DEFAULT_LISTEN_URL;
|
||||
pub use server::ExecServerListenUrlParseError;
|
||||
pub use server::run_main;
|
||||
pub use server::run_main_with_listen_url;
|
||||
|
||||
pub fn configure_arg0_paths(arg0_paths: Arg0DispatchPaths) {
|
||||
local_process::configure_codex_linux_sandbox_exe(arg0_paths.codex_linux_sandbox_exe);
|
||||
}
|
||||
|
||||
@@ -3,13 +3,19 @@ use std::collections::VecDeque;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxExecRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use codex_utils_pty::ExecCommandSession;
|
||||
@@ -48,6 +54,7 @@ use crate::rpc::invalid_request;
|
||||
|
||||
const RETAINED_OUTPUT_BYTES_PER_PROCESS: usize = 1024 * 1024;
|
||||
const NOTIFICATION_CHANNEL_CAPACITY: usize = 256;
|
||||
static CONFIGURED_CODEX_LINUX_SANDBOX_EXE: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
#[cfg(test)]
|
||||
const EXITED_PROCESS_RETENTION: Duration = Duration::from_millis(25);
|
||||
#[cfg(not(test))]
|
||||
@@ -104,6 +111,11 @@ struct ExecServerRuntimeConfig {
|
||||
|
||||
impl ExecServerRuntimeConfig {
|
||||
fn detect() -> Self {
|
||||
if let Some(codex_linux_sandbox_exe) = CONFIGURED_CODEX_LINUX_SANDBOX_EXE.get().cloned() {
|
||||
return Self {
|
||||
codex_linux_sandbox_exe,
|
||||
};
|
||||
}
|
||||
let env_path = std::env::var_os("CODEX_LINUX_SANDBOX_EXE").map(PathBuf::from);
|
||||
let sibling_path = std::env::current_exe().ok().and_then(|current_exe| {
|
||||
current_exe
|
||||
@@ -117,11 +129,14 @@ impl ExecServerRuntimeConfig {
|
||||
}
|
||||
}
|
||||
|
||||
struct PreparedExecLaunch {
|
||||
argv: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
arg0: Option<String>,
|
||||
struct StartedProcess {
|
||||
process_id: ProcessId,
|
||||
sandbox_type: SandboxType,
|
||||
wake_tx: watch::Sender<u64>,
|
||||
}
|
||||
|
||||
pub(crate) fn configure_codex_linux_sandbox_exe(codex_linux_sandbox_exe: Option<PathBuf>) {
|
||||
let _ = CONFIGURED_CODEX_LINUX_SANDBOX_EXE.set(codex_linux_sandbox_exe);
|
||||
}
|
||||
|
||||
impl Default for LocalProcess {
|
||||
@@ -196,15 +211,12 @@ impl LocalProcess {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_process(
|
||||
&self,
|
||||
params: ExecParams,
|
||||
) -> Result<(ExecResponse, watch::Sender<u64>), JSONRPCErrorError> {
|
||||
async fn start_process(&self, params: ExecParams) -> Result<StartedProcess, JSONRPCErrorError> {
|
||||
self.require_initialized_for("exec")?;
|
||||
let process_id = params.process_id.clone();
|
||||
let launch = prepare_exec_launch(¶ms, &self.inner.runtime)?;
|
||||
let (program, args) = launch
|
||||
.argv
|
||||
.command
|
||||
.split_first()
|
||||
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
|
||||
|
||||
@@ -299,19 +311,19 @@ impl LocalProcess {
|
||||
output_notify,
|
||||
));
|
||||
|
||||
Ok((
|
||||
ExecResponse {
|
||||
process_id,
|
||||
sandbox_type: launch.sandbox_type,
|
||||
},
|
||||
Ok(StartedProcess {
|
||||
process_id,
|
||||
sandbox_type: launch.sandbox,
|
||||
wake_tx,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn exec(&self, params: ExecParams) -> Result<ExecResponse, JSONRPCErrorError> {
|
||||
self.start_process(params)
|
||||
.await
|
||||
.map(|(response, _)| response)
|
||||
.map(|started| ExecResponse {
|
||||
process_id: started.process_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn exec_read(
|
||||
@@ -455,17 +467,17 @@ impl LocalProcess {
|
||||
#[async_trait]
|
||||
impl ExecBackend for LocalProcess {
|
||||
async fn start(&self, params: ExecParams) -> Result<StartedExecProcess, ExecServerError> {
|
||||
let (response, wake_tx) = self
|
||||
let started = self
|
||||
.start_process(params)
|
||||
.await
|
||||
.map_err(map_handler_error)?;
|
||||
Ok(StartedExecProcess {
|
||||
process: Arc::new(LocalExecProcess {
|
||||
process_id: response.process_id,
|
||||
process_id: started.process_id,
|
||||
backend: self.clone(),
|
||||
wake_tx,
|
||||
wake_tx: started.wake_tx,
|
||||
}),
|
||||
sandbox_type: response.sandbox_type,
|
||||
sandbox_type: started.sandbox_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -504,6 +516,7 @@ fn build_sandbox_command(
|
||||
argv: &[String],
|
||||
cwd: &Path,
|
||||
env: &HashMap<String, String>,
|
||||
additional_permissions: Option<codex_protocol::models::PermissionProfile>,
|
||||
) -> Result<SandboxCommand, JSONRPCErrorError> {
|
||||
let (program, args) = argv
|
||||
.split_first()
|
||||
@@ -513,25 +526,37 @@ fn build_sandbox_command(
|
||||
args: args.to_vec(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: env.clone(),
|
||||
additional_permissions: None,
|
||||
additional_permissions,
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_exec_launch(
|
||||
params: &ExecParams,
|
||||
runtime: &ExecServerRuntimeConfig,
|
||||
) -> Result<PreparedExecLaunch, JSONRPCErrorError> {
|
||||
) -> Result<SandboxExecRequest, JSONRPCErrorError> {
|
||||
let Some(sandbox) = params.sandbox.as_ref() else {
|
||||
return Ok(PreparedExecLaunch {
|
||||
argv: params.argv.clone(),
|
||||
return Ok(SandboxExecRequest {
|
||||
command: params.argv.clone(),
|
||||
cwd: params.cwd.clone(),
|
||||
env: params.env.clone(),
|
||||
arg0: params.arg0.clone(),
|
||||
sandbox_type: SandboxType::None,
|
||||
network: None,
|
||||
sandbox: SandboxType::None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::unrestricted(),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Enabled,
|
||||
});
|
||||
};
|
||||
|
||||
let command = build_sandbox_command(¶ms.argv, params.cwd.as_path(), ¶ms.env)?;
|
||||
let transformed = sandbox
|
||||
let command = build_sandbox_command(
|
||||
¶ms.argv,
|
||||
params.cwd.as_path(),
|
||||
¶ms.env,
|
||||
sandbox.additional_permissions.clone(),
|
||||
)?;
|
||||
sandbox
|
||||
.transform(
|
||||
command,
|
||||
// TODO: Thread managed-network proxy state across exec-server so
|
||||
@@ -540,14 +565,7 @@ fn prepare_exec_launch(
|
||||
None,
|
||||
runtime.codex_linux_sandbox_exe.as_ref(),
|
||||
)
|
||||
.map_err(|err| internal_error(format!("failed to build sandbox launch: {err}")))?;
|
||||
|
||||
Ok(PreparedExecLaunch {
|
||||
argv: transformed.command,
|
||||
env: transformed.env,
|
||||
arg0: transformed.arg0,
|
||||
sandbox_type: transformed.sandbox,
|
||||
})
|
||||
.map_err(|err| internal_error(format!("failed to build sandbox launch: {err}")))
|
||||
}
|
||||
|
||||
impl LocalProcess {
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::path::PathBuf;
|
||||
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -70,7 +69,6 @@ pub struct ExecParams {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecResponse {
|
||||
pub process_id: ProcessId,
|
||||
pub sandbox_type: SandboxType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use tokio::sync::watch;
|
||||
use tracing::trace;
|
||||
|
||||
@@ -34,18 +35,22 @@ impl RemoteProcess {
|
||||
impl ExecBackend for RemoteProcess {
|
||||
async fn start(&self, params: ExecParams) -> Result<StartedExecProcess, ExecServerError> {
|
||||
let process_id = params.process_id.clone();
|
||||
let sandbox_type = params
|
||||
.sandbox
|
||||
.as_ref()
|
||||
.map_or(SandboxType::None, |sandbox| sandbox.sandbox_type());
|
||||
let session = self.client.register_session(&process_id).await?;
|
||||
let response = match self.client.exec(params).await {
|
||||
Ok(response) => response,
|
||||
match self.client.exec(params).await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
session.unregister().await;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(StartedExecProcess {
|
||||
process: Arc::new(RemoteExecProcess { session }),
|
||||
sandbox_type: response.sandbox_type,
|
||||
sandbox_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,6 @@ pub(crate) async fn exec_server() -> anyhow::Result<ExecServerHarness> {
|
||||
let binary = cargo_bin("codex-exec-server")?;
|
||||
let mut child = Command::new(binary);
|
||||
child.args(["--listen", "ws://127.0.0.1:0"]);
|
||||
if cfg!(target_os = "linux")
|
||||
&& let Ok(sandbox_binary) = cargo_bin("codex-linux-sandbox")
|
||||
{
|
||||
child.env("CODEX_LINUX_SANDBOX_EXE", sandbox_binary);
|
||||
}
|
||||
child.stdin(Stdio::null());
|
||||
child.stdout(Stdio::piped());
|
||||
child.stderr(Stdio::inherit());
|
||||
|
||||
@@ -12,7 +12,15 @@ use codex_exec_server::ExecProcess;
|
||||
use codex_exec_server::ProcessId;
|
||||
use codex_exec_server::ReadResponse;
|
||||
use codex_exec_server::StartedExecProcess;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxLaunchMode;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use test_case::test_case;
|
||||
use tokio::sync::watch;
|
||||
use tokio::time::Duration;
|
||||
@@ -213,6 +221,76 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn platform_sandbox_type() -> SandboxType {
|
||||
if cfg!(target_os = "macos") {
|
||||
SandboxType::MacosSeatbelt
|
||||
} else if cfg!(target_os = "linux") {
|
||||
SandboxType::LinuxSeccomp
|
||||
} else {
|
||||
unreachable!("unix exec-server tests only run on macOS and Linux");
|
||||
}
|
||||
}
|
||||
|
||||
fn write_outside_workspace_sandbox(workspace_root: &std::path::Path) -> SandboxLaunchConfig {
|
||||
let policy = SandboxPolicy::new_workspace_write_policy();
|
||||
SandboxLaunchConfig {
|
||||
mode: SandboxLaunchMode::Require,
|
||||
policy: policy.clone(),
|
||||
file_system_policy: FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
&policy,
|
||||
workspace_root,
|
||||
),
|
||||
network_policy: NetworkSandboxPolicy::from(&policy),
|
||||
sandbox_policy_cwd: workspace_root.to_path_buf(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
use_legacy_landlock: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn assert_exec_process_sandbox_denies_write_outside_workspace(
|
||||
use_remote: bool,
|
||||
) -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let workspace_root = temp_dir.path().join("workspace");
|
||||
std::fs::create_dir(&workspace_root)?;
|
||||
let blocked_path = temp_dir.path().join("blocked.txt");
|
||||
let context = create_process_context(use_remote).await?;
|
||||
let session = context
|
||||
.backend
|
||||
.start(ExecParams {
|
||||
process_id: ProcessId::from("proc-sandbox-denied"),
|
||||
argv: vec![
|
||||
"/usr/bin/python3".to_string(),
|
||||
"-c".to_string(),
|
||||
"from pathlib import Path; import sys; Path(sys.argv[1]).write_text('blocked')"
|
||||
.to_string(),
|
||||
blocked_path.to_string_lossy().into_owned(),
|
||||
],
|
||||
cwd: workspace_root.clone(),
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: Some(write_outside_workspace_sandbox(&workspace_root)),
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(session.sandbox_type, platform_sandbox_type());
|
||||
let StartedExecProcess { process, .. } = session;
|
||||
let wake_rx = process.subscribe_wake();
|
||||
let (_output, exit_code, closed) = collect_process_output_from_reads(process, wake_rx).await?;
|
||||
|
||||
assert_ne!(exit_code, Some(0));
|
||||
assert!(closed);
|
||||
assert!(
|
||||
!blocked_path.exists(),
|
||||
"sandboxed process unexpectedly wrote outside the workspace root"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_exec_process_reports_transport_disconnect() -> Result<()> {
|
||||
let mut context = create_process_context(/*use_remote*/ true).await?;
|
||||
@@ -284,3 +362,10 @@ async fn exec_process_write_then_read(use_remote: bool) -> Result<()> {
|
||||
async fn exec_process_preserves_queued_events_before_subscribe(use_remote: bool) -> Result<()> {
|
||||
assert_exec_process_preserves_queued_events_before_subscribe(use_remote).await
|
||||
}
|
||||
|
||||
#[test_case(false ; "local")]
|
||||
#[test_case(true ; "remote")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_process_sandbox_denies_write_outside_workspace(use_remote: bool) -> Result<()> {
|
||||
assert_exec_process_sandbox_denies_write_outside_workspace(use_remote).await
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_exec_server::ExecResponse;
|
||||
use codex_exec_server::InitializeParams;
|
||||
use codex_exec_server::ProcessId;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use common::exec_server::exec_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -38,16 +37,6 @@ async fn initialize_server(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sandbox_wire_test_mode() -> (&'static str, SandboxType) {
|
||||
if cfg!(target_os = "macos") {
|
||||
("require", SandboxType::MacosSeatbelt)
|
||||
} else if cfg!(target_os = "linux") {
|
||||
("disabled", SandboxType::None)
|
||||
} else {
|
||||
unreachable!("unix exec-server tests only run on macOS and Linux");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> {
|
||||
let mut server = exec_server().await?;
|
||||
@@ -83,68 +72,6 @@ async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> {
|
||||
process_start_response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-1"),
|
||||
sandbox_type: SandboxType::None,
|
||||
}
|
||||
);
|
||||
|
||||
server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_server_starts_sandboxed_process_over_websocket() -> anyhow::Result<()> {
|
||||
let mut server = exec_server().await?;
|
||||
initialize_server(&mut server).await?;
|
||||
|
||||
let cwd = std::env::current_dir()?;
|
||||
let (sandbox_mode, expected_sandbox_type) = sandbox_wire_test_mode();
|
||||
let process_start_id = server
|
||||
.send_request(
|
||||
"process/start",
|
||||
serde_json::json!({
|
||||
"processId": "proc-sandbox",
|
||||
"argv": ["true"],
|
||||
"cwd": cwd,
|
||||
"env": {},
|
||||
"tty": false,
|
||||
"arg0": null,
|
||||
"sandbox": {
|
||||
"mode": sandbox_mode,
|
||||
"policy": {
|
||||
"type": "danger-full-access"
|
||||
},
|
||||
"fileSystemPolicy": {
|
||||
"kind": "unrestricted",
|
||||
"entries": []
|
||||
},
|
||||
"networkPolicy": "enabled",
|
||||
"sandboxPolicyCwd": cwd,
|
||||
"enforceManagedNetwork": false,
|
||||
"windowsSandboxLevel": "disabled",
|
||||
"windowsSandboxPrivateDesktop": false,
|
||||
"useLegacyLandlock": false
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let response = server
|
||||
.wait_for_event(|event| {
|
||||
matches!(
|
||||
event,
|
||||
JSONRPCMessage::Response(JSONRPCResponse { id, .. }) if id == &process_start_id
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(JSONRPCResponse { id, result }) = response else {
|
||||
panic!("expected process/start response");
|
||||
};
|
||||
assert_eq!(id, process_start_id);
|
||||
let process_start_response: ExecResponse = serde_json::from_value(result)?;
|
||||
assert_eq!(
|
||||
process_start_response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-sandbox"),
|
||||
sandbox_type: expected_sandbox_type,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -52,8 +52,9 @@ pub enum SandboxablePreference {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SandboxLaunchMode {
|
||||
Disabled,
|
||||
/// Choose the platform sandbox only when the policy requires it.
|
||||
Auto,
|
||||
/// Always launch with the platform sandbox when one is available.
|
||||
Require,
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ pub struct SandboxLaunchConfig {
|
||||
pub file_system_policy: FileSystemSandboxPolicy,
|
||||
pub network_policy: NetworkSandboxPolicy,
|
||||
pub sandbox_policy_cwd: PathBuf,
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
pub enforce_managed_network: bool,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
@@ -74,7 +76,6 @@ pub struct SandboxLaunchConfig {
|
||||
impl SandboxLaunchConfig {
|
||||
pub fn sandbox_type(&self) -> SandboxType {
|
||||
let preference = match self.mode {
|
||||
SandboxLaunchMode::Disabled => return SandboxType::None,
|
||||
SandboxLaunchMode::Auto => SandboxablePreference::Auto,
|
||||
SandboxLaunchMode::Require => SandboxablePreference::Require,
|
||||
};
|
||||
|
||||
@@ -28,8 +28,10 @@ use tempfile::TempDir;
|
||||
fn assert_seatbelt_denied(stderr: &[u8], path: &Path) {
|
||||
let stderr = String::from_utf8_lossy(stderr);
|
||||
let path_display = path.display().to_string();
|
||||
let denied_path = stderr.contains(&path_display);
|
||||
let denied_operation = stderr.contains("Operation not permitted");
|
||||
assert!(
|
||||
(stderr.contains(&path_display) && stderr.contains("Operation not permitted"))
|
||||
(denied_path && denied_operation)
|
||||
|| stderr.contains("sandbox-exec: sandbox_apply: Operation not permitted"),
|
||||
"unexpected stderr: {stderr}"
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user